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,6 @@
"""
Rides API endpoints for ThrillWiki v1.
This package contains all ride-related API functionality including
ride management, ride photos, and ride-specific operations.
"""

View File

@@ -0,0 +1,147 @@
"""
Ride media serializers for ThrillWiki API v1.
This module contains serializers for ride-specific media functionality.
"""
from rest_framework import serializers
from apps.rides.models import RidePhoto
class RidePhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for ride photos."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
file_size = serializers.ReadOnlyField()
dimensions = serializers.ReadOnlyField()
ride_slug = serializers.CharField(source='ride.slug', read_only=True)
ride_name = serializers.CharField(source='ride.name', read_only=True)
park_slug = serializers.CharField(source='ride.park.slug', read_only=True)
park_name = serializers.CharField(source='ride.park.name', read_only=True)
class Meta:
model = RidePhoto
fields = [
'id',
'image',
'caption',
'alt_text',
'is_primary',
'is_approved',
'photo_type',
'created_at',
'updated_at',
'date_taken',
'uploaded_by_username',
'file_size',
'dimensions',
'ride_slug',
'ride_name',
'park_slug',
'park_name',
]
read_only_fields = [
'id',
'created_at',
'updated_at',
'uploaded_by_username',
'file_size',
'dimensions',
'ride_slug',
'ride_name',
'park_slug',
'park_name',
]
class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
"""Input serializer for creating ride photos."""
class Meta:
model = RidePhoto
fields = [
'image',
'caption',
'alt_text',
'photo_type',
'is_primary',
]
class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
"""Input serializer for updating ride photos."""
class Meta:
model = RidePhoto
fields = [
'caption',
'alt_text',
'photo_type',
'is_primary',
]
class RidePhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for ride photo lists."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
class Meta:
model = RidePhoto
fields = [
'id',
'image',
'caption',
'photo_type',
'is_primary',
'is_approved',
'created_at',
'uploaded_by_username',
]
read_only_fields = fields
class RidePhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for photo approval operations."""
photo_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of photo IDs to approve"
)
approve = serializers.BooleanField(
default=True,
help_text="Whether to approve (True) or reject (False) the photos"
)
class RidePhotoStatsOutputSerializer(serializers.Serializer):
"""Output serializer for ride photo statistics."""
total_photos = serializers.IntegerField()
approved_photos = serializers.IntegerField()
pending_photos = serializers.IntegerField()
has_primary = serializers.BooleanField()
recent_uploads = serializers.IntegerField()
by_type = serializers.DictField(
child=serializers.IntegerField(),
help_text="Photo counts by type"
)
class RidePhotoTypeFilterSerializer(serializers.Serializer):
"""Serializer for filtering photos by type."""
photo_type = serializers.ChoiceField(
choices=[
('exterior', 'Exterior View'),
('queue', 'Queue Area'),
('station', 'Station'),
('onride', 'On-Ride'),
('construction', 'Construction'),
('other', 'Other'),
],
required=False,
help_text="Filter photos by type"
)

View File

@@ -0,0 +1,14 @@
"""
Ride API URLs for ThrillWiki API v1.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RidePhotoViewSet
router = DefaultRouter()
router.register(r"photos", RidePhotoViewSet, basename="ride-photo")
urlpatterns = [
path("", include(router.urls)),
]

View File

@@ -0,0 +1,276 @@
"""
Ride API views for ThrillWiki API v1.
This module contains consolidated ride 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.rides.models import RidePhoto
from apps.rides.services import RideMediaService
from .serializers import (
RidePhotoOutputSerializer,
RidePhotoCreateInputSerializer,
RidePhotoUpdateInputSerializer,
RidePhotoListOutputSerializer,
RidePhotoApprovalInputSerializer,
RidePhotoStatsOutputSerializer,
)
logger = logging.getLogger(__name__)
@extend_schema_view(
list=extend_schema(
summary="List ride photos",
description="Retrieve a paginated list of ride photos with filtering capabilities.",
responses={200: RidePhotoListOutputSerializer(many=True)},
tags=["Ride Media"],
),
create=extend_schema(
summary="Upload ride photo",
description="Upload a new photo for a ride. Requires authentication.",
request=RidePhotoCreateInputSerializer,
responses={
201: RidePhotoOutputSerializer,
400: OpenApiTypes.OBJECT,
401: OpenApiTypes.OBJECT,
},
tags=["Ride Media"],
),
retrieve=extend_schema(
summary="Get ride photo details",
description="Retrieve detailed information about a specific ride photo.",
responses={
200: RidePhotoOutputSerializer,
404: OpenApiTypes.OBJECT,
},
tags=["Ride Media"],
),
update=extend_schema(
summary="Update ride photo",
description="Update ride photo information. Requires authentication and ownership or admin privileges.",
request=RidePhotoUpdateInputSerializer,
responses={
200: RidePhotoOutputSerializer,
400: OpenApiTypes.OBJECT,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
404: OpenApiTypes.OBJECT,
},
tags=["Ride Media"],
),
partial_update=extend_schema(
summary="Partially update ride photo",
description="Partially update ride photo information. Requires authentication and ownership or admin privileges.",
request=RidePhotoUpdateInputSerializer,
responses={
200: RidePhotoOutputSerializer,
400: OpenApiTypes.OBJECT,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
404: OpenApiTypes.OBJECT,
},
tags=["Ride Media"],
),
destroy=extend_schema(
summary="Delete ride photo",
description="Delete a ride photo. Requires authentication and ownership or admin privileges.",
responses={
204: None,
401: OpenApiTypes.OBJECT,
403: OpenApiTypes.OBJECT,
404: OpenApiTypes.OBJECT,
},
tags=["Ride Media"],
),
)
class RidePhotoViewSet(ModelViewSet):
"""
ViewSet for managing ride photos.
Provides CRUD operations for ride photos with proper permission checking.
Uses RideMediaService for business logic operations.
"""
permission_classes = [IsAuthenticated]
lookup_field = "id"
def get_queryset(self):
"""Get photos for the current ride with optimized queries."""
return RidePhoto.objects.select_related(
'ride',
'ride__park',
'uploaded_by'
).filter(
ride_id=self.kwargs.get('ride_pk')
).order_by('-created_at')
def get_serializer_class(self):
"""Return appropriate serializer based on action."""
if self.action == 'list':
return RidePhotoListOutputSerializer
elif self.action == 'create':
return RidePhotoCreateInputSerializer
elif self.action in ['update', 'partial_update']:
return RidePhotoUpdateInputSerializer
else:
return RidePhotoOutputSerializer
def perform_create(self, serializer):
"""Create a new ride photo using RideMediaService."""
ride_id = self.kwargs.get('ride_pk')
if not ride_id:
raise ValidationError("Ride ID is required")
try:
# Use the service to create the photo with proper business logic
photo = RideMediaService.create_photo(
ride_id=ride_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 ride photo: {e}")
raise ValidationError(f"Failed to create photo: {str(e)}")
def perform_update(self, serializer):
"""Update ride 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:
RideMediaService.set_primary_photo(
ride_id=instance.ride_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 ride 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:
RideMediaService.delete_photo(instance.id)
except Exception as e:
logger.error(f"Error deleting ride 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 ride."""
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:
RideMediaService.set_primary_photo(
ride_id=photo.ride_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 = RidePhotoApprovalInputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
photo_ids = serializer.validated_data['photo_ids']
approve = serializer.validated_data['approve']
ride_id = self.kwargs.get('ride_pk')
try:
# Filter photos to only those belonging to this ride
photos = RidePhoto.objects.filter(
id__in=photo_ids,
ride_id=ride_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 ride."""
ride_id = self.kwargs.get('ride_pk')
try:
stats = RideMediaService.get_photo_stats(ride_id=ride_id)
serializer = RidePhotoStatsOutputSerializer(stats)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error getting ride photo stats: {e}")
return Response(
{'error': f'Failed to get photo statistics: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)