Files
thrillwiki_django_no_react/backend/api/v1/parks/views.py

277 lines
9.8 KiB
Python

"""
Park API views for ThrillWiki API v1.
This module contains consolidated park photo viewset for the centralized API structure.
"""
import logging
from django.core.exceptions import PermissionDenied
from drf_spectacular.utils import extend_schema_view, extend_schema
from drf_spectacular.types import OpenApiTypes
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from apps.parks.models import ParkPhoto
from apps.parks.services import ParkMediaService
from .serializers import (
ParkPhotoOutputSerializer,
ParkPhotoCreateInputSerializer,
ParkPhotoUpdateInputSerializer,
ParkPhotoListOutputSerializer,
ParkPhotoApprovalInputSerializer,
ParkPhotoStatsOutputSerializer,
)
logger = logging.getLogger(__name__)
@extend_schema_view(
list=extend_schema(
summary="List park photos",
description="Retrieve a paginated list of park photos with filtering capabilities.",
responses={200: ParkPhotoListOutputSerializer(many=True)},
tags=["Park Media"],
),
create=extend_schema(
summary="Upload park photo",
description="Upload a new photo for a park. Requires authentication.",
request=ParkPhotoCreateInputSerializer,
responses={
201: ParkPhotoOutputSerializer,
400: OpenApiTypes.OBJECT,
401: OpenApiTypes.OBJECT,
},
tags=["Park Media"],
),
retrieve=extend_schema(
summary="Get park photo details",
description="Retrieve detailed information about a specific park photo.",
responses={
200: ParkPhotoOutputSerializer,
404: OpenApiTypes.OBJECT,
},
tags=["Park Media"],
),
update=extend_schema(
summary="Update park photo",
description="Update park photo information. Requires authentication and ownership or admin privileges.",
request=ParkPhotoUpdateInputSerializer,
responses={
200: ParkPhotoOutputSerializer,
400: OpenApiTypes.OBJECT,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
404: OpenApiTypes.OBJECT,
},
tags=["Park Media"],
),
partial_update=extend_schema(
summary="Partially update park photo",
description="Partially update park photo information. Requires authentication and ownership or admin privileges.",
request=ParkPhotoUpdateInputSerializer,
responses={
200: ParkPhotoOutputSerializer,
400: OpenApiTypes.OBJECT,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
404: OpenApiTypes.OBJECT,
},
tags=["Park Media"],
),
destroy=extend_schema(
summary="Delete park photo",
description="Delete a park photo. Requires authentication and ownership or admin privileges.",
responses={
204: None,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
404: OpenApiTypes.OBJECT,
},
tags=["Park Media"],
),
)
class ParkPhotoViewSet(ModelViewSet):
"""
ViewSet for managing park photos.
Provides CRUD operations for park photos with proper permission checking.
Uses ParkMediaService for business logic operations.
"""
permission_classes = [IsAuthenticated]
lookup_field = "id"
def get_queryset(self):
"""Get photos for the current park with optimized queries."""
return ParkPhoto.objects.select_related(
'park',
'park__operator',
'uploaded_by'
).filter(
park_id=self.kwargs.get('park_pk')
).order_by('-created_at')
def get_serializer_class(self):
"""Return appropriate serializer based on action."""
if self.action == 'list':
return ParkPhotoListOutputSerializer
elif self.action == 'create':
return ParkPhotoCreateInputSerializer
elif self.action in ['update', 'partial_update']:
return ParkPhotoUpdateInputSerializer
else:
return ParkPhotoOutputSerializer
def perform_create(self, serializer):
"""Create a new park photo using ParkMediaService."""
park_id = self.kwargs.get('park_pk')
if not park_id:
raise ValidationError("Park ID is required")
try:
# Use the service to create the photo with proper business logic
photo = ParkMediaService.create_photo(
park_id=park_id,
uploaded_by=self.request.user,
**serializer.validated_data
)
# Set the instance for the serializer response
serializer.instance = photo
except Exception as e:
logger.error(f"Error creating park photo: {e}")
raise ValidationError(f"Failed to create photo: {str(e)}")
def perform_update(self, serializer):
"""Update park photo with permission checking."""
instance = self.get_object()
# Check permissions
if not (self.request.user == instance.uploaded_by or self.request.user.is_staff):
raise PermissionDenied("You can only edit your own photos or be an admin.")
# Handle primary photo logic using service
if serializer.validated_data.get('is_primary', False):
try:
ParkMediaService.set_primary_photo(
park_id=instance.park_id,
photo_id=instance.id
)
# Remove is_primary from validated_data since service handles it
if 'is_primary' in serializer.validated_data:
del serializer.validated_data['is_primary']
except Exception as e:
logger.error(f"Error setting primary photo: {e}")
raise ValidationError(f"Failed to set primary photo: {str(e)}")
serializer.save()
def perform_destroy(self, instance):
"""Delete park photo with permission checking."""
# Check permissions
if not (self.request.user == instance.uploaded_by or self.request.user.is_staff):
raise PermissionDenied(
"You can only delete your own photos or be an admin.")
try:
ParkMediaService.delete_photo(instance.id)
except Exception as e:
logger.error(f"Error deleting park photo: {e}")
raise ValidationError(f"Failed to delete photo: {str(e)}")
@action(detail=True, methods=['post'])
def set_primary(self, request, **kwargs):
"""Set this photo as the primary photo for the park."""
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 or be an admin.")
try:
ParkMediaService.set_primary_photo(
park_id=photo.park_id,
photo_id=photo.id
)
# Refresh the photo instance
photo.refresh_from_db()
serializer = self.get_serializer(photo)
return Response(
{
'message': 'Photo set as primary successfully',
'photo': serializer.data
},
status=status.HTTP_200_OK
)
except Exception as e:
logger.error(f"Error setting primary photo: {e}")
return Response(
{'error': f'Failed to set primary photo: {str(e)}'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
def bulk_approve(self, request, **kwargs):
"""Bulk approve or reject multiple photos (admin only)."""
if not request.user.is_staff:
raise PermissionDenied("Only administrators can approve photos.")
serializer = ParkPhotoApprovalInputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
photo_ids = serializer.validated_data['photo_ids']
approve = serializer.validated_data['approve']
park_id = self.kwargs.get('park_pk')
try:
# Filter photos to only those belonging to this park
photos = ParkPhoto.objects.filter(
id__in=photo_ids,
park_id=park_id
)
updated_count = photos.update(is_approved=approve)
return Response(
{
'message': f'Successfully {"approved" if approve else "rejected"} {updated_count} photos',
'updated_count': updated_count
},
status=status.HTTP_200_OK
)
except Exception as e:
logger.error(f"Error in bulk photo approval: {e}")
return Response(
{'error': f'Failed to update photos: {str(e)}'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def stats(self, request, **kwargs):
"""Get photo statistics for the park."""
park_id = self.kwargs.get('park_pk')
try:
stats = ParkMediaService.get_photo_stats(park_id=park_id)
serializer = ParkPhotoStatsOutputSerializer(stats)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error getting park photo stats: {e}")
return Response(
{'error': f'Failed to get photo statistics: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)