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:
pacnpal
2025-08-26 14:40:46 -04:00
parent 831be6a2ee
commit e4e36c7899
133 changed files with 1321 additions and 1001 deletions

View File

@@ -12,15 +12,9 @@ from drf_spectacular.utils import (
OpenApiExample,
)
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.crypto import get_random_string
from django.utils import timezone
from datetime import timedelta
from django.contrib.sites.shortcuts import get_current_site
from django.template.loader import render_to_string
from django.contrib.auth import get_user_model
from apps.accounts.models import UserProfile, TopList, TopListItem
UserModel = get_user_model()
@@ -44,6 +38,7 @@ class ModelChoices:
# === AUTHENTICATION SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
@@ -298,6 +293,7 @@ class AuthStatusOutputSerializer(serializers.Serializer):
# === USER PROFILE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
@@ -388,6 +384,7 @@ class UserProfileUpdateInputSerializer(serializers.Serializer):
# === TOP LIST SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
@@ -447,6 +444,7 @@ class TopListUpdateInputSerializer(serializers.Serializer):
# === TOP LIST ITEM SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(

View File

@@ -21,13 +21,22 @@ urlpatterns = [
path("signup/", views.SignupAPIView.as_view(), name="auth-signup"),
path("logout/", views.LogoutAPIView.as_view(), name="auth-logout"),
path("user/", views.CurrentUserAPIView.as_view(), name="auth-current-user"),
path("password/reset/", views.PasswordResetAPIView.as_view(), name="auth-password-reset"),
path("password/change/", views.PasswordChangeAPIView.as_view(),
name="auth-password-change"),
path("social/providers/", views.SocialProvidersAPIView.as_view(),
name="auth-social-providers"),
path(
"password/reset/",
views.PasswordResetAPIView.as_view(),
name="auth-password-reset",
),
path(
"password/change/",
views.PasswordChangeAPIView.as_view(),
name="auth-password-change",
),
path(
"social/providers/",
views.SocialProvidersAPIView.as_view(),
name="auth-social-providers",
),
path("status/", views.AuthStatusAPIView.as_view(), name="auth-status"),
# Include router URLs for ViewSets (profiles, top lists)
path("", include(router.urls)),
]

View File

@@ -6,12 +6,9 @@ login, signup, logout, password management, social authentication,
user profiles, and top lists.
"""
import time
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from django.db.models import Q
from rest_framework import status
from rest_framework.views import APIView
@@ -20,7 +17,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.decorators import action
from allauth.socialaccount import providers
from drf_spectacular.utils import extend_schema, extend_schema_view
from apps.accounts.models import UserProfile, TopList, TopListItem
@@ -72,6 +68,7 @@ UserModel = get_user_model()
# === AUTHENTICATION API VIEWS ===
@extend_schema_view(
post=extend_schema(
summary="User login",
@@ -250,7 +247,7 @@ class LogoutAPIView(APIView):
{"message": "Logout successful"}
)
return Response(response_serializer.data)
except Exception as e:
except Exception:
return Response(
{"error": "Logout failed"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@@ -361,7 +358,6 @@ class SocialProvidersAPIView(APIView):
def get(self, request: Request) -> Response:
from django.core.cache import cache
from django.contrib.sites.shortcuts import get_current_site
site = get_current_site(request._request) # type: ignore[attr-defined]
@@ -446,6 +442,7 @@ class AuthStatusAPIView(APIView):
# === USER PROFILE API VIEWS ===
class UserProfileViewSet(ModelViewSet):
"""ViewSet for managing user profiles."""
@@ -481,6 +478,7 @@ class UserProfileViewSet(ModelViewSet):
# === TOP LIST API VIEWS ===
class TopListViewSet(ModelViewSet):
"""ViewSet for managing user top lists."""

View File

@@ -12,39 +12,40 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for park 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()
park_slug = serializers.CharField(source='park.slug', read_only=True)
park_name = serializers.CharField(source='park.name', read_only=True)
park_slug = serializers.CharField(source="park.slug", read_only=True)
park_name = serializers.CharField(source="park.name", read_only=True)
class Meta:
model = ParkPhoto
fields = [
'id',
'image',
'caption',
'alt_text',
'is_primary',
'is_approved',
'created_at',
'updated_at',
'date_taken',
'uploaded_by_username',
'file_size',
'dimensions',
'park_slug',
'park_name',
"id",
"image",
"caption",
"alt_text",
"is_primary",
"is_approved",
"created_at",
"updated_at",
"date_taken",
"uploaded_by_username",
"file_size",
"dimensions",
"park_slug",
"park_name",
]
read_only_fields = [
'id',
'created_at',
'updated_at',
'uploaded_by_username',
'file_size',
'dimensions',
'park_slug',
'park_name',
"id",
"created_at",
"updated_at",
"uploaded_by_username",
"file_size",
"dimensions",
"park_slug",
"park_name",
]
@@ -54,10 +55,10 @@ class ParkPhotoCreateInputSerializer(serializers.ModelSerializer):
class Meta:
model = ParkPhoto
fields = [
'image',
'caption',
'alt_text',
'is_primary',
"image",
"caption",
"alt_text",
"is_primary",
]
@@ -67,9 +68,9 @@ class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer):
class Meta:
model = ParkPhoto
fields = [
'caption',
'alt_text',
'is_primary',
"caption",
"alt_text",
"is_primary",
]
@@ -77,18 +78,19 @@ class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for park photo lists."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
source="uploaded_by.username", read_only=True
)
class Meta:
model = ParkPhoto
fields = [
'id',
'image',
'caption',
'is_primary',
'is_approved',
'created_at',
'uploaded_by_username',
"id",
"image",
"caption",
"is_primary",
"is_approved",
"created_at",
"uploaded_by_username",
]
read_only_fields = fields
@@ -97,12 +99,10 @@ class ParkPhotoApprovalInputSerializer(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"
)

View File

@@ -1,6 +1,7 @@
"""
Park API URLs for ThrillWiki API v1.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter

View File

@@ -3,6 +3,7 @@ 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
@@ -108,28 +109,26 @@ class ParkPhotoViewSet(ModelViewSet):
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')
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':
if self.action == "list":
return ParkPhotoListOutputSerializer
elif self.action == 'create':
elif self.action == "create":
return ParkPhotoCreateInputSerializer
elif self.action in ['update', 'partial_update']:
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')
park_id = self.kwargs.get("park_pk")
if not park_id:
raise ValidationError("Park ID is required")
@@ -138,7 +137,7 @@ class ParkPhotoViewSet(ModelViewSet):
photo = ParkMediaService.create_photo(
park_id=park_id,
uploaded_by=self.request.user,
**serializer.validated_data
**serializer.validated_data,
)
# Set the instance for the serializer response
@@ -153,19 +152,20 @@ class ParkPhotoViewSet(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:
ParkMediaService.set_primary_photo(
park_id=instance.park_id,
photo_id=instance.id
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']
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 ParkPhotoViewSet(ModelViewSet):
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):
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:
ParkMediaService.delete_photo(instance.id)
@@ -185,7 +188,7 @@ class ParkPhotoViewSet(ModelViewSet):
logger.error(f"Error deleting park 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 park."""
photo = self.get_object()
@@ -193,13 +196,11 @@ class ParkPhotoViewSet(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:
ParkMediaService.set_primary_photo(
park_id=photo.park_id,
photo_id=photo.id
)
ParkMediaService.set_primary_photo(park_id=photo.park_id, photo_id=photo.id)
# Refresh the photo instance
photo.refresh_from_db()
@@ -207,20 +208,20 @@ class ParkPhotoViewSet(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 ParkPhotoViewSet(ModelViewSet):
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')
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
)
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
"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 park."""
park_id = self.kwargs.get('park_pk')
park_id = self.kwargs.get("park_pk")
try:
stats = ParkMediaService.get_photo_stats(park_id=park_id)
@@ -271,6 +269,6 @@ class ParkPhotoViewSet(ModelViewSet):
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
{"error": f"Failed to get photo statistics: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

View File

@@ -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",
)

View File

@@ -1,6 +1,7 @@
"""
Ride API URLs for ThrillWiki API v1.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter

View File

@@ -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,
)

View File

@@ -9,11 +9,10 @@ from django.urls import path, include
urlpatterns = [
# Domain-specific API endpoints
path('rides/', include('api.v1.rides.urls')),
path('parks/', include('api.v1.parks.urls')),
path('auth/', include('api.v1.auth.urls')),
path("rides/", include("api.v1.rides.urls")),
path("parks/", include("api.v1.parks.urls")),
path("auth/", include("api.v1.auth.urls")),
# Media endpoints (for photo management)
# Will be consolidated from the various media implementations
path('media/', include('api.v1.media.urls')),
path("media/", include("api.v1.media.urls")),
]