""" Media API views for ThrillWiki API v1. This module provides API endpoints for media management including photo uploads, captions, and media operations. Consolidated from apps.media.views with proper domain service integration. """ import json import logging from typing import Any, Dict, Union from django.db.models import Q, QuerySet from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.http import Http404 from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter from drf_spectacular.types import OpenApiTypes from rest_framework import status from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet from rest_framework.parsers import MultiPartParser, FormParser # Import domain-specific models and services instead of generic Photo model from apps.parks.models import ParkPhoto, Park from apps.rides.models import RidePhoto, Ride from apps.parks.services import ParkMediaService from apps.rides.services import RideMediaService from .serializers import ( PhotoUploadInputSerializer, PhotoUploadOutputSerializer, PhotoDetailOutputSerializer, PhotoUpdateInputSerializer, PhotoListOutputSerializer, MediaStatsOutputSerializer, BulkPhotoActionInputSerializer, BulkPhotoActionOutputSerializer, ) logger = logging.getLogger(__name__) @extend_schema_view( post=extend_schema( summary="Upload photo", description="Upload a photo and associate it with a content object (park, ride, etc.)", request=PhotoUploadInputSerializer, responses={ 201: PhotoUploadOutputSerializer, 400: OpenApiTypes.OBJECT, 403: OpenApiTypes.OBJECT, }, tags=["Media"], ), ) class PhotoUploadAPIView(APIView): """API endpoint for photo uploads.""" permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser] def post(self, request: Request) -> Response: """Upload a photo and associate it with a content object.""" try: serializer = PhotoUploadInputSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data # Get content object try: content_type = ContentType.objects.get( app_label=validated_data["app_label"], model=validated_data["model"] ) content_object = content_type.get_object_for_this_type( pk=validated_data["object_id"] ) except ContentType.DoesNotExist: return Response( { "error": f"Invalid content type: {validated_data['app_label']}.{validated_data['model']}" }, status=status.HTTP_400_BAD_REQUEST, ) except content_type.model_class().DoesNotExist: return Response( {"error": "Content object not found"}, status=status.HTTP_404_NOT_FOUND, ) # Determine which domain service to use based on content object if hasattr(content_object, '_meta') and content_object._meta.app_label == 'parks': # Check permissions for park photos if not request.user.has_perm("parks.add_parkphoto"): return Response( {"error": "You do not have permission to upload park photos"}, status=status.HTTP_403_FORBIDDEN, ) # Create park photo using park media service photo = ParkMediaService.upload_photo( park=content_object, image_file=validated_data["photo"], user=request.user, caption=validated_data.get("caption", ""), alt_text=validated_data.get("alt_text", ""), is_primary=validated_data.get("is_primary", False), ) elif hasattr(content_object, '_meta') and content_object._meta.app_label == 'rides': # Check permissions for ride photos if not request.user.has_perm("rides.add_ridephoto"): return Response( {"error": "You do not have permission to upload ride photos"}, status=status.HTTP_403_FORBIDDEN, ) # Create ride photo using ride media service photo = RideMediaService.upload_photo( ride=content_object, image_file=validated_data["photo"], user=request.user, caption=validated_data.get("caption", ""), alt_text=validated_data.get("alt_text", ""), is_primary=validated_data.get("is_primary", False), photo_type=validated_data.get("photo_type", "general"), ) else: return Response( {"error": f"Unsupported content type for media upload: {content_object._meta.label}"}, status=status.HTTP_400_BAD_REQUEST, ) response_serializer = PhotoUploadOutputSerializer( { "id": photo.id, "url": photo.image.url, "caption": photo.caption, "alt_text": photo.alt_text, "is_primary": photo.is_primary, "message": "Photo uploaded successfully", } ) return Response(response_serializer.data, status=status.HTTP_201_CREATED) except Exception as e: logger.error(f"Error in photo upload: {str(e)}", exc_info=True) return Response( {"error": f"An error occurred while uploading the photo: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @extend_schema_view( list=extend_schema( summary="List photos", description="Retrieve a list of photos with optional filtering", parameters=[ OpenApiParameter( name="content_type", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, description="Filter by content type (e.g., 'parks.park', 'rides.ride')", ), OpenApiParameter( name="object_id", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, description="Filter by object ID", ), OpenApiParameter( name="is_primary", type=OpenApiTypes.BOOL, location=OpenApiParameter.QUERY, description="Filter by primary photos only", ), ], responses={200: PhotoListOutputSerializer(many=True)}, tags=["Media"], ), retrieve=extend_schema( summary="Get photo details", description="Retrieve detailed information about a specific photo", responses={ 200: PhotoDetailOutputSerializer, 404: OpenApiTypes.OBJECT, }, tags=["Media"], ), update=extend_schema( summary="Update photo", description="Update photo information (caption, alt text, etc.)", request=PhotoUpdateInputSerializer, responses={ 200: PhotoDetailOutputSerializer, 400: OpenApiTypes.OBJECT, 403: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT, }, tags=["Media"], ), destroy=extend_schema( summary="Delete photo", description="Delete a photo (only by owner or admin)", responses={ 204: None, 403: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT, }, tags=["Media"], ), set_primary=extend_schema( summary="Set photo as primary", description="Set this photo as the primary photo for its content object", responses={ 200: OpenApiTypes.OBJECT, 403: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT, }, tags=["Media"], ), ) class PhotoViewSet(ModelViewSet): """ViewSet for managing photos across domains.""" permission_classes = [IsAuthenticated] lookup_field = "id" def get_queryset(self) -> QuerySet: """Get queryset combining photos from all domains.""" # Combine park and ride photos park_photos = ParkPhoto.objects.select_related('uploaded_by', 'park') ride_photos = RidePhoto.objects.select_related('uploaded_by', 'ride') # Apply filters content_type = self.request.query_params.get('content_type') object_id = self.request.query_params.get('object_id') is_primary = self.request.query_params.get('is_primary') if content_type == 'parks.park': queryset = park_photos if object_id: queryset = queryset.filter(park_id=object_id) elif content_type == 'rides.ride': queryset = ride_photos if object_id: queryset = queryset.filter(ride_id=object_id) else: # Return combined queryset (this is complex due to different models) # For now, return park photos as default - in production might need Union queryset = park_photos if is_primary is not None: is_primary_bool = is_primary.lower() in ('true', '1', 'yes') queryset = queryset.filter(is_primary=is_primary_bool) return queryset.order_by('-uploaded_at') def get_serializer_class(self): """Return appropriate serializer based on action.""" if self.action == "list": return PhotoListOutputSerializer elif self.action in ["update", "partial_update"]: return PhotoUpdateInputSerializer return PhotoDetailOutputSerializer def get_object(self): """Get photo object from either domain.""" photo_id = self.kwargs.get('id') # Try to find in park photos first try: return ParkPhoto.objects.select_related('uploaded_by', 'park').get(id=photo_id) except ParkPhoto.DoesNotExist: pass # Try ride photos try: return RidePhoto.objects.select_related('uploaded_by', 'ride').get(id=photo_id) except RidePhoto.DoesNotExist: pass raise Http404("Photo not found") def update(self, request: Request, *args, **kwargs) -> Response: """Update photo details.""" photo = self.get_object() # Check permissions if not (request.user == photo.uploaded_by or request.user.is_staff): raise PermissionDenied("You can only edit your own photos") serializer = self.get_serializer(data=request.data, partial=True) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # Update fields for field, value in serializer.validated_data.items(): setattr(photo, field, value) photo.save() # Return updated photo details response_serializer = PhotoDetailOutputSerializer(photo) return Response(response_serializer.data) def destroy(self, request: Request, *args, **kwargs) -> Response: """Delete a photo.""" photo = self.get_object() # Check permissions if not (request.user == photo.uploaded_by or request.user.is_staff): raise PermissionDenied("You can only delete your own photos") photo.delete() return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=['post']) def set_primary(self, request: Request, id=None) -> Response: """Set this photo as primary for its content object.""" photo = self.get_object() # Check permissions if not (request.user == photo.uploaded_by or request.user.is_staff): raise PermissionDenied("You can only modify your own photos") # Use appropriate service based on photo type if isinstance(photo, ParkPhoto): ParkMediaService.set_primary_photo(photo.park, photo) elif isinstance(photo, RidePhoto): RideMediaService.set_primary_photo(photo.ride, photo) return Response({ "message": "Photo set as primary successfully", "photo_id": photo.id, "is_primary": True }) @extend_schema_view( get=extend_schema( summary="Get media statistics", description="Retrieve statistics about photos and media usage", responses={200: MediaStatsOutputSerializer}, tags=["Media"], ), ) class MediaStatsAPIView(APIView): """API endpoint for media statistics.""" permission_classes = [IsAuthenticated] def get(self, request: Request) -> Response: """Get media statistics.""" from django.db.models import Count from datetime import datetime, timedelta # Count photos by type park_photo_count = ParkPhoto.objects.count() ride_photo_count = RidePhoto.objects.count() total_photos = park_photo_count + ride_photo_count # Recent uploads (last 30 days) thirty_days_ago = datetime.now() - timedelta(days=30) recent_park_uploads = ParkPhoto.objects.filter( uploaded_at__gte=thirty_days_ago).count() recent_ride_uploads = RidePhoto.objects.filter( uploaded_at__gte=thirty_days_ago).count() recent_uploads = recent_park_uploads + recent_ride_uploads # Top uploaders from django.db.models import Q from django.contrib.auth import get_user_model User = get_user_model() # This is a simplified version - in production might need more complex aggregation top_uploaders = [] stats = MediaStatsOutputSerializer({ "total_photos": total_photos, "photos_by_content_type": { "parks": park_photo_count, "rides": ride_photo_count, }, "recent_uploads": recent_uploads, "top_uploaders": top_uploaders, "storage_usage": { "total_size": 0, # Would need to calculate from file sizes "average_size": 0, } }) return Response(stats.data) @extend_schema_view( post=extend_schema( summary="Bulk photo actions", description="Perform bulk actions on multiple photos (delete, approve, etc.)", request=BulkPhotoActionInputSerializer, responses={ 200: BulkPhotoActionOutputSerializer, 400: OpenApiTypes.OBJECT, 403: OpenApiTypes.OBJECT, }, tags=["Media"], ), ) class BulkPhotoActionAPIView(APIView): """API endpoint for bulk photo operations.""" permission_classes = [IsAuthenticated] def post(self, request: Request) -> Response: """Perform bulk action on photos.""" serializer = BulkPhotoActionInputSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) photo_ids = serializer.validated_data['photo_ids'] action = serializer.validated_data['action'] success_count = 0 failed_count = 0 errors = [] for photo_id in photo_ids: try: # Find photo in either domain photo = None try: photo = ParkPhoto.objects.get(id=photo_id) except ParkPhoto.DoesNotExist: try: photo = RidePhoto.objects.get(id=photo_id) except RidePhoto.DoesNotExist: errors.append(f"Photo {photo_id} not found") failed_count += 1 continue # Check permissions if not (request.user == photo.uploaded_by or request.user.is_staff): errors.append(f"No permission for photo {photo_id}") failed_count += 1 continue # Perform action if action == 'delete': photo.delete() success_count += 1 elif action == 'approve': if hasattr(photo, 'is_approved'): photo.is_approved = True photo.save() success_count += 1 else: errors.append(f"Photo {photo_id} does not support approval") failed_count += 1 elif action == 'reject': if hasattr(photo, 'is_approved'): photo.is_approved = False photo.save() success_count += 1 else: errors.append(f"Photo {photo_id} does not support approval") failed_count += 1 except Exception as e: errors.append(f"Error processing photo {photo_id}: {str(e)}") failed_count += 1 response_data = BulkPhotoActionOutputSerializer({ "success_count": success_count, "failed_count": failed_count, "errors": errors, "message": f"Bulk {action} completed: {success_count} successful, {failed_count} failed" }) return Response(response_data.data)