Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-08-26 13:19:04 -04:00
parent bf7e0c0f40
commit 831be6a2ee
151 changed files with 16260 additions and 9137 deletions

View File

@@ -0,0 +1,484 @@
"""
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)