diff --git a/api/serializers.py b/api/serializers.py index c0b081a..a26b7e6 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from tournaments.models.court import Court -from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken, UnregisteredTeam, UnregisteredPlayer +from tournaments.models import Club, LiveMatch, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, FailedApiCall, DateInterval, Log, DeviceToken, UnregisteredTeam, UnregisteredPlayer, Image from django.db.utils import IntegrityError from django.conf import settings @@ -239,3 +239,17 @@ class UnregisteredPlayerSerializer(serializers.ModelSerializer): model = UnregisteredPlayer fields = '__all__' # ['id', 'team_registration_id', 'first_name', 'last_name', 'licence_id', 'rank', 'has_paid'] + +class ImageSerializer(serializers.ModelSerializer): + image_url = serializers.SerializerMethodField() + + def get_image_url(self, obj): + if obj.image: + return self.context['request'].build_absolute_uri(obj.image.url) + return None + + class Meta: + model = Image + fields = ['id', 'title', 'description', 'image', 'image_url', 'uploaded_at', + 'event', 'image_type'] + read_only_fields = ['id', 'uploaded_at', 'image_url'] diff --git a/api/urls.py b/api/urls.py index e054f87..2a73de4 100644 --- a/api/urls.py +++ b/api/urls.py @@ -11,6 +11,7 @@ router.register(r'users', views.UserViewSet) router.register(r'user-names', views.ShortUserViewSet) router.register(r'clubs', views.ClubViewSet) router.register(r'tournaments', views.TournamentViewSet) +router.register(r'images', views.ImageViewSet) router.register(r'events', views.EventViewSet) router.register(r'rounds', views.RoundViewSet) router.register(r'group-stages', views.GroupStageViewSet) diff --git a/api/views.py b/api/views.py index be20311..e17f741 100644 --- a/api/views.py +++ b/api/views.py @@ -1,5 +1,5 @@ -from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer -from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer +from .serializers import ClubSerializer, CourtSerializer, DateIntervalSerializer, DrawLogSerializer, TournamentSerializer, UserSerializer, EventSerializer, RoundSerializer, GroupStageSerializer, MatchSerializer, TeamScoreSerializer, TeamRegistrationSerializer, PlayerRegistrationSerializer, PurchaseSerializer, ShortUserSerializer, FailedApiCallSerializer, LogSerializer, DeviceTokenSerializer, CustomUserSerializer, UnregisteredTeamSerializer, UnregisteredPlayerSerializer, ImageSerializer +from tournaments.models import Club, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamScore, TeamRegistration, PlayerRegistration, Court, DateInterval, Purchase, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image from rest_framework import viewsets from rest_framework.response import Response @@ -300,6 +300,34 @@ class ShortUserViewSet(viewsets.ModelViewSet): serializer_class = ShortUserSerializer permission_classes = [] # Users are public whereas the other requests are only for logged users +class ImageViewSet(viewsets.ModelViewSet): + """ + Viewset for handling event image uploads and retrieval. + + This allows umpires/organizers to upload images for events from the iOS app, + which can then be displayed on the event pages. + """ + serializer_class = ImageSerializer + queryset = Image.objects.all() + + def get_queryset(self): + queryset = Image.objects.all() + + # Filter by event + event_id = self.request.query_params.get('event_id') + image_type = self.request.query_params.get('image_type') + + if event_id: + queryset = queryset.filter(event_id=event_id) + if image_type: + queryset = queryset.filter(image_type=image_type) + + return queryset + + def perform_create(self, serializer): + serializer.save() + + @api_view(['POST']) @permission_classes([IsAuthenticated]) def process_refund(request, team_registration_id): diff --git a/padelclub_backend/settings.py b/padelclub_backend/settings.py index b6c8262..31e51fb 100644 --- a/padelclub_backend/settings.py +++ b/padelclub_backend/settings.py @@ -147,6 +147,10 @@ USE_L10N = True STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static/') +# Media files (User uploads) +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') + # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field diff --git a/padelclub_backend/settings_app.py b/padelclub_backend/settings_app.py index e69f82e..c10611e 100644 --- a/padelclub_backend/settings_app.py +++ b/padelclub_backend/settings_app.py @@ -46,7 +46,7 @@ QR_CODE_CACHE_ALIAS = 'qr-code' SYNC_APPS = { 'sync': {}, - 'tournaments': { 'exclude': ['Log', 'FailedApiCall', 'DeviceToken'] } + 'tournaments': { 'exclude': ['Log', 'FailedApiCall', 'DeviceToken', 'Image'] } } STRIPE_CURRENCY = 'eur' diff --git a/padelclub_backend/urls.py b/padelclub_backend/urls.py index 85c8b15..abb883a 100644 --- a/padelclub_backend/urls.py +++ b/padelclub_backend/urls.py @@ -15,6 +15,8 @@ Including another URLconf """ from django.contrib import admin from django.urls import include, path +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ @@ -27,3 +29,7 @@ urlpatterns = [ path('dj-auth/', include('django.contrib.auth.urls')), ] + +# Serve media files in development +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/requirements.txt b/requirements.txt index cc14798..f0cf5e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ django-filter==24.3 cryptography==41.0.7 stripe==11.6.0 django-background-tasks==1.2.8 +Pillow==10.2.0 diff --git a/tournaments/admin.py b/tournaments/admin.py index 5cc7cfb..c9f1039 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -6,7 +6,7 @@ from django.utils.html import escape from django.urls import reverse from django.utils.safestring import mark_safe -from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer +from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image from .forms import CustomUserCreationForm, CustomUserChangeForm from .filters import TeamScoreTournamentListFilter, MatchTournamentListFilter, SimpleTournamentListFilter, MatchTypeListFilter, SimpleIndexListFilter, StartDateRangeFilter @@ -45,10 +45,39 @@ class CustomUserAdmin(UserAdmin): super().save_model(request, obj, form, change) class EventAdmin(SyncedObjectAdmin): - list_display = ['creation_date', 'name', 'club', 'creator', 'creator_full_name', 'tenup_id'] + list_display = ['creation_date', 'name', 'club', 'creator', 'creator_full_name', 'tenup_id', 'display_images'] list_filter = ['creator', 'tenup_id'] raw_id_fields = ['creator'] ordering = ['-creation_date'] + readonly_fields = ['display_images_preview'] + + fieldsets = [ + (None, {'fields': ['name', 'club', 'creator', 'creation_date', 'tenup_id']}), + ('Images', {'fields': ['display_images_preview'], 'classes': ['collapse']}), + ] + + def display_images(self, obj): + count = obj.images.count() + return count if count > 0 else '-' + display_images.short_description = 'Images' + + def display_images_preview(self, obj): + html = '
+ {image.title or "Untitled"}
+ Type: {image.get_image_type_display()}
+
No images uploaded for this event.
' + return mark_safe(html) + display_images_preview.short_description = 'Images Preview' class TournamentAdmin(SyncedObjectAdmin): list_display = ['display_name', 'event', 'is_private', 'start_date', 'payment', 'creator'] @@ -157,6 +186,39 @@ class UnregisteredPlayerAdmin(admin.ModelAdmin): list_filter = [] ordering = ['last_name', 'first_name'] +class ImageAdmin(admin.ModelAdmin): + list_display = ['title', 'event', 'image_type', 'order', 'uploaded_at', 'file_size', 'image_preview_small'] + list_filter = ['event', 'image_type', 'uploaded_at'] + search_fields = ['title', 'description', 'event__name'] + ordering = ['order'] + readonly_fields = ['id', 'uploaded_at', 'image_preview', 'file_size'] + raw_id_fields = ['event'] + + def image_preview(self, obj): + if obj.image: + return mark_safe(f'