mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 02:11:09 -05:00
Add migrations for ParkPhoto and RidePhoto models with associated events
- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model. - Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent. - Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto. - Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes. - Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
This commit is contained in:
@@ -12,46 +12,47 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
||||
"""Output serializer for ride photos."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source='uploaded_by.username', read_only=True)
|
||||
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)
|
||||
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',
|
||||
"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',
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"uploaded_by_username",
|
||||
"file_size",
|
||||
"dimensions",
|
||||
"ride_slug",
|
||||
"ride_name",
|
||||
"park_slug",
|
||||
"park_name",
|
||||
]
|
||||
|
||||
|
||||
@@ -61,11 +62,11 @@ class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RidePhoto
|
||||
fields = [
|
||||
'image',
|
||||
'caption',
|
||||
'alt_text',
|
||||
'photo_type',
|
||||
'is_primary',
|
||||
"image",
|
||||
"caption",
|
||||
"alt_text",
|
||||
"photo_type",
|
||||
"is_primary",
|
||||
]
|
||||
|
||||
|
||||
@@ -75,10 +76,10 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RidePhoto
|
||||
fields = [
|
||||
'caption',
|
||||
'alt_text',
|
||||
'photo_type',
|
||||
'is_primary',
|
||||
"caption",
|
||||
"alt_text",
|
||||
"photo_type",
|
||||
"is_primary",
|
||||
]
|
||||
|
||||
|
||||
@@ -86,19 +87,20 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer):
|
||||
"""Simplified output serializer for ride photo lists."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source='uploaded_by.username', read_only=True)
|
||||
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',
|
||||
"id",
|
||||
"image",
|
||||
"caption",
|
||||
"photo_type",
|
||||
"is_primary",
|
||||
"is_approved",
|
||||
"created_at",
|
||||
"uploaded_by_username",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -107,12 +109,10 @@ 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"
|
||||
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"
|
||||
default=True, help_text="Whether to approve (True) or reject (False) the photos"
|
||||
)
|
||||
|
||||
|
||||
@@ -125,8 +125,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer):
|
||||
has_primary = serializers.BooleanField()
|
||||
recent_uploads = serializers.IntegerField()
|
||||
by_type = serializers.DictField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="Photo counts by type"
|
||||
child=serializers.IntegerField(), help_text="Photo counts by type"
|
||||
)
|
||||
|
||||
|
||||
@@ -135,13 +134,13 @@ class RidePhotoTypeFilterSerializer(serializers.Serializer):
|
||||
|
||||
photo_type = serializers.ChoiceField(
|
||||
choices=[
|
||||
('exterior', 'Exterior View'),
|
||||
('queue', 'Queue Area'),
|
||||
('station', 'Station'),
|
||||
('onride', 'On-Ride'),
|
||||
('construction', 'Construction'),
|
||||
('other', 'Other'),
|
||||
("exterior", "Exterior View"),
|
||||
("queue", "Queue Area"),
|
||||
("station", "Station"),
|
||||
("onride", "On-Ride"),
|
||||
("construction", "Construction"),
|
||||
("other", "Other"),
|
||||
],
|
||||
required=False,
|
||||
help_text="Filter photos by type"
|
||||
help_text="Filter photos by type",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Ride API URLs for ThrillWiki API v1.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -108,28 +109,26 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
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')
|
||||
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':
|
||||
if self.action == "list":
|
||||
return RidePhotoListOutputSerializer
|
||||
elif self.action == 'create':
|
||||
elif self.action == "create":
|
||||
return RidePhotoCreateInputSerializer
|
||||
elif self.action in ['update', 'partial_update']:
|
||||
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')
|
||||
ride_id = self.kwargs.get("ride_pk")
|
||||
if not ride_id:
|
||||
raise ValidationError("Ride ID is required")
|
||||
|
||||
@@ -138,7 +137,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
photo = RideMediaService.create_photo(
|
||||
ride_id=ride_id,
|
||||
uploaded_by=self.request.user,
|
||||
**serializer.validated_data
|
||||
**serializer.validated_data,
|
||||
)
|
||||
|
||||
# Set the instance for the serializer response
|
||||
@@ -153,19 +152,20 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
instance = self.get_object()
|
||||
|
||||
# Check permissions
|
||||
if not (self.request.user == instance.uploaded_by or self.request.user.is_staff):
|
||||
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):
|
||||
if serializer.validated_data.get("is_primary", False):
|
||||
try:
|
||||
RideMediaService.set_primary_photo(
|
||||
ride_id=instance.ride_id,
|
||||
photo_id=instance.id
|
||||
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']
|
||||
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)}")
|
||||
@@ -175,9 +175,12 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
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):
|
||||
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.")
|
||||
"You can only delete your own photos or be an admin."
|
||||
)
|
||||
|
||||
try:
|
||||
RideMediaService.delete_photo(instance.id)
|
||||
@@ -185,7 +188,7 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
logger.error(f"Error deleting ride photo: {e}")
|
||||
raise ValidationError(f"Failed to delete photo: {str(e)}")
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
@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()
|
||||
@@ -193,13 +196,11 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
# 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.")
|
||||
"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
|
||||
)
|
||||
RideMediaService.set_primary_photo(ride_id=photo.ride_id, photo_id=photo.id)
|
||||
|
||||
# Refresh the photo instance
|
||||
photo.refresh_from_db()
|
||||
@@ -207,20 +208,20 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
|
||||
return Response(
|
||||
{
|
||||
'message': 'Photo set as primary successfully',
|
||||
'photo': serializer.data
|
||||
"message": "Photo set as primary successfully",
|
||||
"photo": serializer.data,
|
||||
},
|
||||
status=status.HTTP_200_OK
|
||||
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
|
||||
{"error": f"Failed to set primary photo: {str(e)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
|
||||
@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:
|
||||
@@ -229,38 +230,35 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
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')
|
||||
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
|
||||
)
|
||||
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
|
||||
"message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
|
||||
"updated_count": updated_count,
|
||||
},
|
||||
status=status.HTTP_200_OK
|
||||
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
|
||||
{"error": f"Failed to update photos: {str(e)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
@action(detail=False, methods=["get"])
|
||||
def stats(self, request, **kwargs):
|
||||
"""Get photo statistics for the ride."""
|
||||
ride_id = self.kwargs.get('ride_pk')
|
||||
ride_id = self.kwargs.get("ride_pk")
|
||||
|
||||
try:
|
||||
stats = RideMediaService.get_photo_stats(ride_id=ride_id)
|
||||
@@ -271,6 +269,6 @@ class RidePhotoViewSet(ModelViewSet):
|
||||
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
|
||||
{"error": f"Failed to get photo statistics: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user