feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -17,6 +17,7 @@ class ParkHistoryViewSet(viewsets.GenericViewSet):
"""
ViewSet for retrieving park history.
"""
permission_classes = [AllowAny]
lookup_field = "slug"
lookup_url_kwarg = "park_slug"
@@ -40,12 +41,7 @@ class ParkHistoryViewSet(viewsets.GenericViewSet):
"last_modified": events.first().pgh_created_at if len(events) else None,
}
data = {
"park": park,
"current_state": park,
"summary": summary,
"events": events
}
data = {"park": park, "current_state": park, "summary": summary, "events": events}
serializer = ParkHistoryOutputSerializer(data)
return Response(serializer.data)
@@ -55,6 +51,7 @@ class RideHistoryViewSet(viewsets.GenericViewSet):
"""
ViewSet for retrieving ride history.
"""
permission_classes = [AllowAny]
lookup_field = "slug"
lookup_url_kwarg = "ride_slug"
@@ -79,12 +76,7 @@ class RideHistoryViewSet(viewsets.GenericViewSet):
"last_modified": events.first().pgh_created_at if len(events) else None,
}
data = {
"ride": ride,
"current_state": ride,
"summary": summary,
"events": events
}
data = {"ride": ride, "current_state": ride, "summary": summary, "events": events}
serializer = RideHistoryOutputSerializer(data)
return Response(serializer.data)

View File

@@ -65,14 +65,12 @@ class ParkReviewViewSet(ModelViewSet):
def get_permissions(self):
"""Set permissions based on action."""
permission_classes = [AllowAny] if self.action in ['list', 'retrieve', 'stats'] else [IsAuthenticated]
permission_classes = [AllowAny] if self.action in ["list", "retrieve", "stats"] else [IsAuthenticated]
return [permission() for permission in permission_classes]
def get_queryset(self):
"""Get reviews for the current park."""
queryset = ParkReview.objects.select_related(
"park", "user", "user__profile"
)
queryset = ParkReview.objects.select_related("park", "user", "user__profile")
park_slug = self.kwargs.get("park_slug")
if park_slug:
@@ -82,7 +80,7 @@ class ParkReviewViewSet(ModelViewSet):
except Park.DoesNotExist:
return queryset.none()
if not (hasattr(self.request, 'user') and getattr(self.request.user, 'is_staff', False)):
if not (hasattr(self.request, "user") and getattr(self.request.user, "is_staff", False)):
queryset = queryset.filter(is_published=True)
return queryset.order_by("-created_at")
@@ -102,16 +100,12 @@ class ParkReviewViewSet(ModelViewSet):
try:
park, _ = Park.get_by_slug(park_slug)
except Park.DoesNotExist:
raise NotFound("Park not found")
raise NotFound("Park not found") from None
if ParkReview.objects.filter(park=park, user=self.request.user).exists():
raise ValidationError("You have already reviewed this park")
serializer.save(
park=park,
user=self.request.user,
is_published=True
)
serializer.save(park=park, user=self.request.user, is_published=True)
def perform_update(self, serializer):
instance = self.get_object()
@@ -134,17 +128,18 @@ class ParkReviewViewSet(ModelViewSet):
try:
park, _ = Park.get_by_slug(park_slug)
except Park.DoesNotExist:
return Response({"error": "Park not found"}, status=status.HTTP_404_NOT_FOUND)
return Response({"detail": "Park not found"}, status=status.HTTP_404_NOT_FOUND)
reviews = ParkReview.objects.filter(park=park, is_published=True)
total_reviews = reviews.count()
avg_rating = reviews.aggregate(avg=Avg('rating'))['avg']
avg_rating = reviews.aggregate(avg=Avg("rating"))["avg"]
rating_distribution = {}
for i in range(1, 11):
rating_distribution[str(i)] = reviews.filter(rating=i).count()
from datetime import timedelta
recent_reviews = reviews.filter(created_at__gte=timezone.now() - timedelta(days=30)).count()
stats = {

View File

@@ -21,6 +21,7 @@ from rest_framework.views import APIView
try:
from apps.parks.models import Park
from apps.rides.models import Ride
MODELS_AVAILABLE = True
except Exception:
Park = None # type: ignore
@@ -31,6 +32,7 @@ except Exception:
try:
from apps.api.v1.serializers.parks import ParkDetailOutputSerializer
from apps.api.v1.serializers.rides import RideDetailOutputSerializer, RideListOutputSerializer
SERIALIZERS_AVAILABLE = True
except Exception:
SERIALIZERS_AVAILABLE = False
@@ -52,22 +54,41 @@ class ParkRidesListAPIView(APIView):
description="Get paginated list of rides at a specific park with filtering options",
parameters=[
# Pagination
OpenApiParameter(name="page", location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT, description="Page number"),
OpenApiParameter(name="page_size", location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT, description="Number of results per page (max 100)"),
OpenApiParameter(
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Page number"
),
OpenApiParameter(
name="page_size",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.INT,
description="Number of results per page (max 100)",
),
# Filtering
OpenApiParameter(name="category", location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR, description="Filter by ride category"),
OpenApiParameter(name="status", location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR, description="Filter by operational status"),
OpenApiParameter(name="search", location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR, description="Search rides by name"),
OpenApiParameter(
name="category",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
description="Filter by ride category",
),
OpenApiParameter(
name="status",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
description="Filter by operational status",
),
OpenApiParameter(
name="search",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
description="Search rides by name",
),
# Ordering
OpenApiParameter(name="ordering", location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR, description="Order results by field"),
OpenApiParameter(
name="ordering",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
description="Order results by field",
),
],
responses={
200: OpenApiTypes.OBJECT,
@@ -87,12 +108,14 @@ class ParkRidesListAPIView(APIView):
try:
park, is_historical = Park.get_by_slug(park_slug)
except Park.DoesNotExist:
raise NotFound("Park not found")
raise NotFound("Park not found") from None
# Get rides for this park
qs = Ride.objects.filter(park=park).select_related(
"manufacturer", "designer", "ride_model", "park_area"
).prefetch_related("photos")
qs = (
Ride.objects.filter(park=park)
.select_related("manufacturer", "designer", "ride_model", "park_area")
.prefetch_related("photos")
)
# Apply filtering
qs = self._apply_filters(qs, request.query_params)
@@ -107,9 +130,7 @@ class ParkRidesListAPIView(APIView):
page = paginator.paginate_queryset(qs, request)
if SERIALIZERS_AVAILABLE:
serializer = RideListOutputSerializer(
page, many=True, context={"request": request, "park": park}
)
serializer = RideListOutputSerializer(page, many=True, context={"request": request, "park": park})
return paginator.get_paginated_response(serializer.data)
else:
# Fallback serialization
@@ -145,9 +166,7 @@ class ParkRidesListAPIView(APIView):
search = params.get("search")
if search:
qs = qs.filter(
Q(name__icontains=search) |
Q(description__icontains=search) |
Q(manufacturer__name__icontains=search)
Q(name__icontains=search) | Q(description__icontains=search) | Q(manufacturer__name__icontains=search)
)
return qs
@@ -179,42 +198,46 @@ class ParkRideDetailAPIView(APIView):
try:
park, is_historical = Park.get_by_slug(park_slug)
except Park.DoesNotExist:
raise NotFound("Park not found")
raise NotFound("Park not found") from None
# Get the ride
try:
ride, is_historical = Ride.get_by_slug(ride_slug, park=park)
except Ride.DoesNotExist:
raise NotFound("Ride not found at this park")
raise NotFound("Ride not found at this park") from None
# Ensure ride belongs to this park
if ride.park_id != park.id:
raise NotFound("Ride not found at this park")
if SERIALIZERS_AVAILABLE:
serializer = RideDetailOutputSerializer(
ride, context={"request": request, "park": park}
)
serializer = RideDetailOutputSerializer(ride, context={"request": request, "park": park})
return Response(serializer.data)
else:
# Fallback serialization
return Response({
"id": ride.id,
"name": ride.name,
"slug": ride.slug,
"description": getattr(ride, "description", ""),
"category": getattr(ride, "category", ""),
"status": getattr(ride, "status", ""),
"park": {
"id": park.id,
"name": park.name,
"slug": park.slug,
},
"manufacturer": {
"name": ride.manufacturer.name if ride.manufacturer else "",
"slug": getattr(ride.manufacturer, "slug", "") if ride.manufacturer else "",
} if ride.manufacturer else None,
})
return Response(
{
"id": ride.id,
"name": ride.name,
"slug": ride.slug,
"description": getattr(ride, "description", ""),
"category": getattr(ride, "category", ""),
"status": getattr(ride, "status", ""),
"park": {
"id": park.id,
"name": park.name,
"slug": park.slug,
},
"manufacturer": (
{
"name": ride.manufacturer.name if ride.manufacturer else "",
"slug": getattr(ride.manufacturer, "slug", "") if ride.manufacturer else "",
}
if ride.manufacturer
else None
),
}
)
class ParkComprehensiveDetailAPIView(APIView):
@@ -243,25 +266,21 @@ class ParkComprehensiveDetailAPIView(APIView):
try:
park, is_historical = Park.get_by_slug(park_slug)
except Park.DoesNotExist:
raise NotFound("Park not found")
raise NotFound("Park not found") from None
# Get park with full related data
park = Park.objects.select_related(
"operator", "property_owner", "location"
).prefetch_related(
"areas", "rides", "photos"
).get(pk=park.pk)
park = (
Park.objects.select_related("operator", "property_owner", "location")
.prefetch_related("areas", "rides", "photos")
.get(pk=park.pk)
)
# Get a sample of rides (first 10) for preview
rides_sample = Ride.objects.filter(park=park).select_related(
"manufacturer", "designer", "ride_model"
)[:10]
rides_sample = Ride.objects.filter(park=park).select_related("manufacturer", "designer", "ride_model")[:10]
if SERIALIZERS_AVAILABLE:
# Get full park details
park_serializer = ParkDetailOutputSerializer(
park, context={"request": request}
)
park_serializer = ParkDetailOutputSerializer(park, context={"request": request})
park_data = park_serializer.data
# Add rides summary
@@ -279,25 +298,27 @@ class ParkComprehensiveDetailAPIView(APIView):
return Response(park_data)
else:
# Fallback serialization
return Response({
"id": park.id,
"name": park.name,
"slug": park.slug,
"description": getattr(park, "description", ""),
"location": str(getattr(park, "location", "")),
"operator": getattr(park.operator, "name", "") if hasattr(park, "operator") else "",
"ride_count": getattr(park, "ride_count", 0),
"rides_summary": {
"total_count": getattr(park, "ride_count", 0),
"sample": [
{
"id": ride.id,
"name": ride.name,
"slug": ride.slug,
"category": getattr(ride, "category", ""),
}
for ride in rides_sample
],
"full_list_url": f"/api/v1/parks/{park_slug}/rides/",
},
})
return Response(
{
"id": park.id,
"name": park.name,
"slug": park.slug,
"description": getattr(park, "description", ""),
"location": str(getattr(park, "location", "")),
"operator": getattr(park.operator, "name", "") if hasattr(park, "operator") else "",
"ride_count": getattr(park, "ride_count", 0),
"rides_summary": {
"total_count": getattr(park, "ride_count", 0),
"sample": [
{
"id": ride.id,
"name": ride.name,
"slug": ride.slug,
"category": getattr(ride, "category", ""),
}
for ride in rides_sample
],
"full_list_url": f"/api/v1/parks/{park_slug}/rides/",
},
}
)

File diff suppressed because it is too large Load Diff

View File

@@ -116,14 +116,12 @@ class RidePhotoViewSet(ModelViewSet):
def get_permissions(self):
"""Set permissions based on action."""
permission_classes = [AllowAny] if self.action in ['list', 'retrieve', 'stats'] else [IsAuthenticated]
permission_classes = [AllowAny] if self.action in ["list", "retrieve", "stats"] else [IsAuthenticated]
return [permission() for permission in permission_classes]
def get_queryset(self):
"""Get photos for the current ride with optimized queries."""
queryset = RidePhoto.objects.select_related(
"ride", "ride__park", "ride__park__operator", "uploaded_by"
)
queryset = RidePhoto.objects.select_related("ride", "ride__park", "ride__park__operator", "uploaded_by")
# Filter by park and ride from URL kwargs
park_slug = self.kwargs.get("park_slug")
@@ -163,9 +161,9 @@ class RidePhotoViewSet(ModelViewSet):
park, _ = Park.get_by_slug(park_slug)
ride, _ = Ride.get_by_slug(ride_slug, park=park)
except Park.DoesNotExist:
raise NotFound("Park not found")
raise NotFound("Park not found") from None
except Ride.DoesNotExist:
raise NotFound("Ride not found at this park")
raise NotFound("Ride not found at this park") from None
try:
# Use the service to create the photo with proper business logic
@@ -187,17 +185,14 @@ class RidePhotoViewSet(ModelViewSet):
except Exception as e:
logger.error(f"Error creating ride photo: {e}")
raise ValidationError(f"Failed to create photo: {str(e)}")
raise ValidationError(f"Failed to create photo: {str(e)}") from None
def perform_update(self, serializer):
"""Update ride photo with permission checking."""
instance = self.get_object()
# Check permissions - allow owner or staff
if not (
self.request.user == instance.uploaded_by
or getattr(self.request.user, "is_staff", False)
):
if not (self.request.user == instance.uploaded_by or getattr(self.request.user, "is_staff", False)):
raise PermissionDenied("You can only edit your own photos or be an admin.")
# Handle primary photo logic using service
@@ -209,48 +204,40 @@ class RidePhotoViewSet(ModelViewSet):
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)}")
raise ValidationError(f"Failed to set primary photo: {str(e)}") from None
try:
serializer.save()
logger.info(f"Updated ride photo {instance.id} by user {self.request.user.username}")
except Exception as e:
logger.error(f"Error updating ride photo: {e}")
raise ValidationError(f"Failed to update photo: {str(e)}")
raise ValidationError(f"Failed to update photo: {str(e)}") from None
def perform_destroy(self, instance):
"""Delete ride photo with permission checking."""
# Check permissions - allow owner or staff
if not (
self.request.user == instance.uploaded_by
or getattr(self.request.user, "is_staff", False)
):
raise PermissionDenied(
"You can only delete your own photos or be an admin."
)
if not (self.request.user == instance.uploaded_by or getattr(self.request.user, "is_staff", False)):
raise PermissionDenied("You can only delete your own photos or be an admin.")
try:
# Delete from Cloudflare first if image exists
if instance.image:
try:
from django_cloudflareimages_toolkit.services import CloudflareImagesService
service = CloudflareImagesService()
service.delete_image(instance.image)
logger.info(
f"Successfully deleted ride photo from Cloudflare: {instance.image.cloudflare_id}")
logger.info(f"Successfully deleted ride photo from Cloudflare: {instance.image.cloudflare_id}")
except Exception as e:
logger.error(
f"Failed to delete ride photo from Cloudflare: {str(e)}")
logger.error(f"Failed to delete ride photo from Cloudflare: {str(e)}")
# Continue with database deletion even if Cloudflare deletion fails
RideMediaService.delete_photo(
instance, deleted_by=self.request.user
)
RideMediaService.delete_photo(instance, deleted_by=self.request.user)
logger.info(f"Deleted ride photo {instance.id} by user {self.request.user.username}")
except Exception as e:
logger.error(f"Error deleting ride photo: {e}")
raise ValidationError(f"Failed to delete photo: {str(e)}")
raise ValidationError(f"Failed to delete photo: {str(e)}") from None
@extend_schema(
summary="Set photo as primary",
@@ -269,13 +256,8 @@ class RidePhotoViewSet(ModelViewSet):
photo = self.get_object()
# Check permissions - allow owner or staff
if not (
request.user == photo.uploaded_by
or getattr(request.user, "is_staff", False)
):
raise PermissionDenied(
"You can only modify your own photos or be an admin."
)
if not (request.user == photo.uploaded_by or getattr(request.user, "is_staff", False)):
raise PermissionDenied("You can only modify your own photos or be an admin.")
try:
success = RideMediaService.set_primary_photo(ride=photo.ride, photo=photo)
@@ -287,21 +269,21 @@ class RidePhotoViewSet(ModelViewSet):
return Response(
{
"message": "Photo set as primary successfully",
"detail": "Photo set as primary successfully",
"photo": serializer.data,
},
status=status.HTTP_200_OK,
)
else:
return Response(
{"error": "Failed to set primary photo"},
{"detail": "Failed to set primary photo"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
logger.error(f"Error setting primary photo: {e}")
return Response(
{"error": f"Failed to set primary photo: {str(e)}"},
{"detail": f"Failed to set primary photo: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -334,7 +316,7 @@ class RidePhotoViewSet(ModelViewSet):
if photo_ids is None or approve is None:
return Response(
{"error": "Missing required fields: photo_ids and/or approve."},
{"detail": "Missing required fields: photo_ids and/or approve."},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -350,7 +332,7 @@ class RidePhotoViewSet(ModelViewSet):
return Response(
{
"message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
"detail": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
"updated_count": updated_count,
},
status=status.HTTP_200_OK,
@@ -359,7 +341,7 @@ class RidePhotoViewSet(ModelViewSet):
except Exception as e:
logger.error(f"Error in bulk photo approval: {e}")
return Response(
{"error": f"Failed to update photos: {str(e)}"},
{"detail": f"Failed to update photos: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -381,7 +363,7 @@ class RidePhotoViewSet(ModelViewSet):
if not park_slug or not ride_slug:
return Response(
{"error": "Park and ride slugs are required"},
{"detail": "Park and ride slugs are required"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -390,12 +372,12 @@ class RidePhotoViewSet(ModelViewSet):
ride, _ = Ride.get_by_slug(ride_slug, park=park)
except Park.DoesNotExist:
return Response(
{"error": "Park not found"},
{"detail": "Park not found"},
status=status.HTTP_404_NOT_FOUND,
)
except Ride.DoesNotExist:
return Response(
{"error": "Ride not found at this park"},
{"detail": "Ride not found at this park"},
status=status.HTTP_404_NOT_FOUND,
)
@@ -407,7 +389,7 @@ 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)}"},
{"detail": f"Failed to get photo statistics: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -431,7 +413,7 @@ class RidePhotoViewSet(ModelViewSet):
if not park_slug or not ride_slug:
return Response(
{"error": "Park and ride slugs are required"},
{"detail": "Park and ride slugs are required"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -440,19 +422,19 @@ class RidePhotoViewSet(ModelViewSet):
ride, _ = Ride.get_by_slug(ride_slug, park=park)
except Park.DoesNotExist:
return Response(
{"error": "Park not found"},
{"detail": "Park not found"},
status=status.HTTP_404_NOT_FOUND,
)
except Ride.DoesNotExist:
return Response(
{"error": "Ride not found at this park"},
{"detail": "Ride not found at this park"},
status=status.HTTP_404_NOT_FOUND,
)
cloudflare_image_id = request.data.get("cloudflare_image_id")
if not cloudflare_image_id:
return Response(
{"error": "cloudflare_image_id is required"},
{"detail": "cloudflare_image_id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -469,27 +451,25 @@ class RidePhotoViewSet(ModelViewSet):
if not image_data:
return Response(
{"error": "Image not found in Cloudflare"},
{"detail": "Image not found in Cloudflare"},
status=status.HTTP_400_BAD_REQUEST,
)
# Try to find existing CloudflareImage record by cloudflare_id
cloudflare_image = None
try:
cloudflare_image = CloudflareImage.objects.get(
cloudflare_id=cloudflare_image_id)
cloudflare_image = CloudflareImage.objects.get(cloudflare_id=cloudflare_image_id)
# Update existing record with latest data from Cloudflare
cloudflare_image.status = 'uploaded'
cloudflare_image.status = "uploaded"
cloudflare_image.uploaded_at = timezone.now()
cloudflare_image.metadata = image_data.get('meta', {})
cloudflare_image.metadata = image_data.get("meta", {})
# Extract variants from nested result structure
cloudflare_image.variants = image_data.get(
'result', {}).get('variants', [])
cloudflare_image.variants = image_data.get("result", {}).get("variants", [])
cloudflare_image.cloudflare_metadata = image_data
cloudflare_image.width = image_data.get('width')
cloudflare_image.height = image_data.get('height')
cloudflare_image.format = image_data.get('format', '')
cloudflare_image.width = image_data.get("width")
cloudflare_image.height = image_data.get("height")
cloudflare_image.format = image_data.get("format", "")
cloudflare_image.save()
except CloudflareImage.DoesNotExist:
@@ -497,24 +477,23 @@ class RidePhotoViewSet(ModelViewSet):
cloudflare_image = CloudflareImage.objects.create(
cloudflare_id=cloudflare_image_id,
user=request.user,
status='uploaded',
upload_url='', # Not needed for uploaded images
status="uploaded",
upload_url="", # Not needed for uploaded images
expires_at=timezone.now() + timezone.timedelta(days=365), # Set far future expiry
uploaded_at=timezone.now(),
metadata=image_data.get('meta', {}),
metadata=image_data.get("meta", {}),
# Extract variants from nested result structure
variants=image_data.get('result', {}).get('variants', []),
variants=image_data.get("result", {}).get("variants", []),
cloudflare_metadata=image_data,
width=image_data.get('width'),
height=image_data.get('height'),
format=image_data.get('format', ''),
width=image_data.get("width"),
height=image_data.get("height"),
format=image_data.get("format", ""),
)
except Exception as api_error:
logger.error(
f"Error fetching image from Cloudflare API: {str(api_error)}", exc_info=True)
logger.error(f"Error fetching image from Cloudflare API: {str(api_error)}", exc_info=True)
return Response(
{"error": f"Failed to fetch image from Cloudflare: {str(api_error)}"},
{"detail": f"Failed to fetch image from Cloudflare: {str(api_error)}"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -544,6 +523,6 @@ class RidePhotoViewSet(ModelViewSet):
except Exception as e:
logger.error(f"Error saving ride photo: {e}")
return Response(
{"error": f"Failed to save photo: {str(e)}"},
{"detail": f"Failed to save photo: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@@ -115,14 +115,12 @@ class RideReviewViewSet(ModelViewSet):
def get_permissions(self):
"""Set permissions based on action."""
permission_classes = [AllowAny] if self.action in ['list', 'retrieve', 'stats'] else [IsAuthenticated]
permission_classes = [AllowAny] if self.action in ["list", "retrieve", "stats"] else [IsAuthenticated]
return [permission() for permission in permission_classes]
def get_queryset(self):
"""Get reviews for the current ride with optimized queries."""
queryset = RideReview.objects.select_related(
"ride", "ride__park", "user", "user__profile"
)
queryset = RideReview.objects.select_related("ride", "ride__park", "user", "user__profile")
# Filter by park and ride from URL kwargs
park_slug = self.kwargs.get("park_slug")
@@ -138,8 +136,7 @@ class RideReviewViewSet(ModelViewSet):
return queryset.none()
# Filter published reviews for non-staff users
if not (hasattr(self.request, 'user') and
getattr(self.request.user, 'is_staff', False)):
if not (hasattr(self.request, "user") and getattr(self.request.user, "is_staff", False)):
queryset = queryset.filter(is_published=True)
return queryset.order_by("-created_at")
@@ -167,9 +164,9 @@ class RideReviewViewSet(ModelViewSet):
park, _ = Park.get_by_slug(park_slug)
ride, _ = Ride.get_by_slug(ride_slug, park=park)
except Park.DoesNotExist:
raise NotFound("Park not found")
raise NotFound("Park not found") from None
except Ride.DoesNotExist:
raise NotFound("Ride not found at this park")
raise NotFound("Ride not found at this park") from None
# Check if user already has a review for this ride
if RideReview.objects.filter(ride=ride, user=self.request.user).exists():
@@ -178,26 +175,21 @@ class RideReviewViewSet(ModelViewSet):
try:
# Save the review
review = serializer.save(
ride=ride,
user=self.request.user,
is_published=True # Auto-publish for now, can add moderation later
ride=ride, user=self.request.user, is_published=True # Auto-publish for now, can add moderation later
)
logger.info(f"Created ride review {review.id} for ride {ride.name} by user {self.request.user.username}")
except Exception as e:
logger.error(f"Error creating ride review: {e}")
raise ValidationError(f"Failed to create review: {str(e)}")
raise ValidationError(f"Failed to create review: {str(e)}") from None
def perform_update(self, serializer):
"""Update ride review with permission checking."""
instance = self.get_object()
# Check permissions - allow owner or staff
if not (
self.request.user == instance.user
or getattr(self.request.user, "is_staff", False)
):
if not (self.request.user == instance.user or getattr(self.request.user, "is_staff", False)):
raise PermissionDenied("You can only edit your own reviews or be an admin.")
try:
@@ -205,15 +197,12 @@ class RideReviewViewSet(ModelViewSet):
logger.info(f"Updated ride review {instance.id} by user {self.request.user.username}")
except Exception as e:
logger.error(f"Error updating ride review: {e}")
raise ValidationError(f"Failed to update review: {str(e)}")
raise ValidationError(f"Failed to update review: {str(e)}") from None
def perform_destroy(self, instance):
"""Delete ride review with permission checking."""
# Check permissions - allow owner or staff
if not (
self.request.user == instance.user
or getattr(self.request.user, "is_staff", False)
):
if not (self.request.user == instance.user or getattr(self.request.user, "is_staff", False)):
raise PermissionDenied("You can only delete your own reviews or be an admin.")
try:
@@ -221,7 +210,7 @@ class RideReviewViewSet(ModelViewSet):
instance.delete()
except Exception as e:
logger.error(f"Error deleting ride review: {e}")
raise ValidationError(f"Failed to delete review: {str(e)}")
raise ValidationError(f"Failed to delete review: {str(e)}") from None
@extend_schema(
summary="Get ride review statistics",
@@ -241,7 +230,7 @@ class RideReviewViewSet(ModelViewSet):
if not park_slug or not ride_slug:
return Response(
{"error": "Park and ride slugs are required"},
{"detail": "Park and ride slugs are required"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -250,12 +239,12 @@ class RideReviewViewSet(ModelViewSet):
ride, _ = Ride.get_by_slug(ride_slug, park=park)
except Park.DoesNotExist:
return Response(
{"error": "Park not found"},
{"detail": "Park not found"},
status=status.HTTP_404_NOT_FOUND,
)
except Ride.DoesNotExist:
return Response(
{"error": "Ride not found at this park"},
{"detail": "Ride not found at this park"},
status=status.HTTP_404_NOT_FOUND,
)
@@ -268,7 +257,7 @@ class RideReviewViewSet(ModelViewSet):
pending_reviews = RideReview.objects.filter(ride=ride, is_published=False).count()
# Calculate average rating
avg_rating = reviews.aggregate(avg_rating=Avg('rating'))['avg_rating']
avg_rating = reviews.aggregate(avg_rating=Avg("rating"))["avg_rating"]
# Get rating distribution
rating_distribution = {}
@@ -277,6 +266,7 @@ class RideReviewViewSet(ModelViewSet):
# Get recent reviews count (last 30 days)
from datetime import timedelta
thirty_days_ago = timezone.now() - timedelta(days=30)
recent_reviews = reviews.filter(created_at__gte=thirty_days_ago).count()
@@ -295,7 +285,7 @@ class RideReviewViewSet(ModelViewSet):
except Exception as e:
logger.error(f"Error getting ride review stats: {e}")
return Response(
{"error": f"Failed to get review statistics: {str(e)}"},
{"detail": f"Failed to get review statistics: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -340,7 +330,7 @@ class RideReviewViewSet(ModelViewSet):
is_published=True,
moderated_by=request.user,
moderated_at=timezone.now(),
moderation_notes=moderation_notes
moderation_notes=moderation_notes,
)
message = f"Successfully published {updated_count} reviews"
elif action_type == "unpublish":
@@ -348,7 +338,7 @@ class RideReviewViewSet(ModelViewSet):
is_published=False,
moderated_by=request.user,
moderated_at=timezone.now(),
moderation_notes=moderation_notes
moderation_notes=moderation_notes,
)
message = f"Successfully unpublished {updated_count} reviews"
elif action_type == "delete":
@@ -357,13 +347,13 @@ class RideReviewViewSet(ModelViewSet):
message = f"Successfully deleted {updated_count} reviews"
else:
return Response(
{"error": "Invalid action type"},
{"detail": "Invalid action type"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(
{
"message": message,
"detail": message,
"updated_count": updated_count,
},
status=status.HTTP_200_OK,
@@ -372,6 +362,6 @@ class RideReviewViewSet(ModelViewSet):
except Exception as e:
logger.error(f"Error in bulk review moderation: {e}")
return Response(
{"error": f"Failed to moderate reviews: {str(e)}"},
{"detail": f"Failed to moderate reviews: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@@ -50,18 +50,14 @@ from apps.parks.models import Park, ParkPhoto
class ParkPhotoOutputSerializer(serializers.ModelSerializer):
"""Enhanced output serializer for park photos with Cloudflare Images support."""
uploaded_by_username = serializers.CharField(
source="uploaded_by.username", read_only=True
)
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
file_size = serializers.SerializerMethodField()
dimensions = serializers.SerializerMethodField()
image_url = serializers.SerializerMethodField()
image_variants = serializers.SerializerMethodField()
@extend_schema_field(
serializers.IntegerField(allow_null=True, help_text="File size in bytes")
)
@extend_schema_field(serializers.IntegerField(allow_null=True, help_text="File size in bytes"))
def get_file_size(self, obj):
"""Get file size in bytes."""
return obj.file_size
@@ -79,11 +75,7 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer):
"""Get image dimensions as [width, height]."""
return obj.dimensions
@extend_schema_field(
serializers.URLField(
help_text="Full URL to the Cloudflare Images asset", allow_null=True
)
)
@extend_schema_field(serializers.URLField(help_text="Full URL to the Cloudflare Images asset", allow_null=True))
def get_image_url(self, obj):
"""Get the full Cloudflare Images URL."""
if obj.image:
@@ -175,9 +167,7 @@ class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer):
class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
"""Optimized output serializer for park photo lists."""
uploaded_by_username = serializers.CharField(
source="uploaded_by.username", read_only=True
)
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
class Meta:
model = ParkPhoto
@@ -196,12 +186,8 @@ class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
class ParkPhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for bulk 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"
)
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 ParkPhotoStatsOutputSerializer(serializers.Serializer):
@@ -261,7 +247,7 @@ class HybridParkSerializer(serializers.ModelSerializer):
def get_city(self, obj):
"""Get city from related location."""
try:
return obj.location.city if hasattr(obj, 'location') and obj.location else None
return obj.location.city if hasattr(obj, "location") and obj.location else None
except AttributeError:
return None
@@ -269,7 +255,7 @@ class HybridParkSerializer(serializers.ModelSerializer):
def get_state(self, obj):
"""Get state from related location."""
try:
return obj.location.state if hasattr(obj, 'location') and obj.location else None
return obj.location.state if hasattr(obj, "location") and obj.location else None
except AttributeError:
return None
@@ -277,7 +263,7 @@ class HybridParkSerializer(serializers.ModelSerializer):
def get_country(self, obj):
"""Get country from related location."""
try:
return obj.location.country if hasattr(obj, 'location') and obj.location else None
return obj.location.country if hasattr(obj, "location") and obj.location else None
except AttributeError:
return None
@@ -285,7 +271,7 @@ class HybridParkSerializer(serializers.ModelSerializer):
def get_continent(self, obj):
"""Get continent from related location."""
try:
return obj.location.continent if hasattr(obj, 'location') and obj.location else None
return obj.location.continent if hasattr(obj, "location") and obj.location else None
except AttributeError:
return None
@@ -293,7 +279,7 @@ class HybridParkSerializer(serializers.ModelSerializer):
def get_latitude(self, obj):
"""Get latitude from related location."""
try:
if hasattr(obj, 'location') and obj.location and obj.location.coordinates:
if hasattr(obj, "location") and obj.location and obj.location.coordinates:
return obj.location.coordinates[1] # PostGIS returns [lon, lat]
return None
except (AttributeError, IndexError, TypeError):
@@ -303,7 +289,7 @@ class HybridParkSerializer(serializers.ModelSerializer):
def get_longitude(self, obj):
"""Get longitude from related location."""
try:
if hasattr(obj, 'location') and obj.location and obj.location.coordinates:
if hasattr(obj, "location") and obj.location and obj.location.coordinates:
return obj.location.coordinates[0] # PostGIS returns [lon, lat]
return None
except (AttributeError, IndexError, TypeError):
@@ -333,13 +319,11 @@ class HybridParkSerializer(serializers.ModelSerializer):
"description",
"status",
"park_type",
# Dates and computed fields
"opening_date",
"closing_date",
"opening_year",
"operating_season",
# Location fields
"city",
"state",
@@ -347,28 +331,22 @@ class HybridParkSerializer(serializers.ModelSerializer):
"continent",
"latitude",
"longitude",
# Company relationships
"operator_name",
"property_owner_name",
# Statistics
"size_acres",
"average_rating",
"ride_count",
"coaster_count",
# Images
"banner_image_url",
"card_image_url",
# URLs
"website",
"url",
# Computed fields for filtering
"search_text",
# Metadata
"created_at",
"updated_at",

View File

@@ -46,8 +46,8 @@ ride_photos_router.register(r"", RidePhotoViewSet, basename="ride-photo")
ride_reviews_router = DefaultRouter()
ride_reviews_router.register(r"", RideReviewViewSet, basename="ride-review")
from .history_views import ParkHistoryViewSet, RideHistoryViewSet
from .park_reviews_views import ParkReviewViewSet
from .history_views import ParkHistoryViewSet, RideHistoryViewSet # noqa: E402
from .park_reviews_views import ParkReviewViewSet # noqa: E402
# Create routers for nested park endpoints
reviews_router = DefaultRouter()
@@ -59,11 +59,9 @@ app_name = "api_v1_parks"
urlpatterns = [
# Core list/create endpoints
path("", ParkListCreateAPIView.as_view(), name="park-list-create"),
# Hybrid filtering endpoints
path("hybrid/", HybridParkAPIView.as_view(), name="park-hybrid-list"),
path("hybrid/filter-metadata/", ParkFilterMetadataAPIView.as_view(), name="park-hybrid-filter-metadata"),
# Filter options
path("filter-options/", FilterOptionsAPIView.as_view(), name="park-filter-options"),
# Autocomplete / suggestion endpoints
@@ -79,14 +77,11 @@ urlpatterns = [
),
# Detail and action endpoints - supports both ID and slug
path("<str:pk>/", ParkDetailAPIView.as_view(), name="park-detail"),
# Park rides endpoints
path("<str:park_slug>/rides/", ParkRidesListAPIView.as_view(), name="park-rides-list"),
path("<str:park_slug>/rides/<str:ride_slug>/", ParkRideDetailAPIView.as_view(), name="park-ride-detail"),
# Comprehensive park detail endpoint with rides summary
path("<str:park_slug>/detail/", ParkComprehensiveDetailAPIView.as_view(), name="park-comprehensive-detail"),
# Park image settings endpoint
path(
"<int:pk>/image-settings/",
@@ -95,33 +90,29 @@ urlpatterns = [
),
# Park photo endpoints - domain-specific photo management
path("<str:park_pk>/photos/", include(router.urls)),
# Nested ride photo endpoints - photos for specific rides within parks
path("<str:park_slug>/rides/<str:ride_slug>/photos/", include(ride_photos_router.urls)),
# Nested ride review endpoints - reviews for specific rides within parks
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
# Nested ride review endpoints - reviews for specific rides within parks
path("<str:park_slug>/rides/<str:ride_slug>/reviews/", include(ride_reviews_router.urls)),
# Ride History
path("<str:park_slug>/rides/<str:ride_slug>/history/", RideHistoryViewSet.as_view({'get': 'list'}), name="ride-history"),
path(
"<str:park_slug>/rides/<str:ride_slug>/history/",
RideHistoryViewSet.as_view({"get": "list"}),
name="ride-history",
),
# Park Reviews
path("<str:park_slug>/reviews/", include(reviews_router.urls)),
# Park History
path("<str:park_slug>/history/", ParkHistoryViewSet.as_view({'get': 'list'}), name="park-history"),
path("<str:park_slug>/history/", ParkHistoryViewSet.as_view({"get": "list"}), name="park-history"),
# Roadtrip API endpoints
path("roadtrip/create/", CreateTripView.as_view(), name="roadtrip-create"),
path("roadtrip/find-along-route/", FindParksAlongRouteView.as_view(), name="roadtrip-find"),
path("roadtrip/geocode/", GeocodeAddressView.as_view(), name="roadtrip-geocode"),
path("roadtrip/distance/", ParkDistanceCalculatorView.as_view(), name="roadtrip-distance"),
# Operator endpoints
path("operators/", OperatorListAPIView.as_view(), name="operator-list"),
# Location search endpoints
path("search/location/", location_search, name="location-search"),
path("search/reverse-geocode/", reverse_geocode, name="reverse-geocode"),

View File

@@ -134,9 +134,7 @@ class ParkPhotoViewSet(ModelViewSet):
def get_queryset(self): # type: ignore[override]
"""Get photos for the current park with optimized queries."""
queryset = ParkPhoto.objects.select_related(
"park", "park__operator", "uploaded_by"
)
queryset = ParkPhoto.objects.select_related("park", "park__operator", "uploaded_by")
# If park_pk is provided in URL kwargs, filter by park
# If park_pk is provided in URL kwargs, filter by park
@@ -172,7 +170,7 @@ class ParkPhotoViewSet(ModelViewSet):
# Use real park ID
park_id = park.id
except Park.DoesNotExist:
raise ValidationError("Park not found")
raise ValidationError("Park not found") from None
try:
# Use the service to create the photo with proper business logic
@@ -188,48 +186,38 @@ class ParkPhotoViewSet(ModelViewSet):
except (ValidationException, ValidationError) as e:
logger.warning(f"Validation error creating park photo: {e}")
raise ValidationError(str(e))
raise ValidationError(str(e)) from None
except ServiceError as e:
logger.error(f"Service error creating park photo: {e}")
raise ValidationError(f"Failed to create photo: {str(e)}")
raise ValidationError(f"Failed to create photo: {str(e)}") from None
def perform_update(self, serializer):
"""Update park photo with permission checking."""
instance = self.get_object()
# Check permissions - allow owner or staff
if not (
self.request.user == instance.uploaded_by
or cast(Any, self.request.user).is_staff
):
if not (self.request.user == instance.uploaded_by or cast(Any, 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
)
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 (ValidationException, ValidationError) as e:
logger.warning(f"Validation error setting primary photo: {e}")
raise ValidationError(str(e))
raise ValidationError(str(e)) from None
except ServiceError as e:
logger.error(f"Service error setting primary photo: {e}")
raise ValidationError(f"Failed to set primary photo: {str(e)}")
raise ValidationError(f"Failed to set primary photo: {str(e)}") from None
def perform_destroy(self, instance):
"""Delete park photo with permission checking."""
# Check permissions - allow owner or staff
if not (
self.request.user == instance.uploaded_by
or cast(Any, self.request.user).is_staff
):
raise PermissionDenied(
"You can only delete your own photos or be an admin."
)
if not (self.request.user == instance.uploaded_by or cast(Any, self.request.user).is_staff):
raise PermissionDenied("You can only delete your own photos or be an admin.")
# Delete from Cloudflare first if image exists
if instance.image:
@@ -240,9 +228,7 @@ class ParkPhotoViewSet(ModelViewSet):
service = CloudflareImagesService()
service.delete_image(instance.image)
logger.info(
f"Successfully deleted park photo from Cloudflare: {instance.image.cloudflare_id}"
)
logger.info(f"Successfully deleted park photo from Cloudflare: {instance.image.cloudflare_id}")
except ImportError:
logger.warning("CloudflareImagesService not available")
except ServiceError as e:
@@ -250,12 +236,10 @@ class ParkPhotoViewSet(ModelViewSet):
# Continue with database deletion even if Cloudflare deletion fails
try:
ParkMediaService().delete_photo(
instance.id, deleted_by=cast(UserModel, self.request.user)
)
ParkMediaService().delete_photo(instance.id, deleted_by=cast(UserModel, self.request.user))
except ServiceError as e:
logger.error(f"Service error deleting park photo: {e}")
raise ValidationError(f"Failed to delete photo: {str(e)}")
raise ValidationError(f"Failed to delete photo: {str(e)}") from None
@extend_schema(
summary="Set photo as primary",
@@ -275,14 +259,10 @@ class ParkPhotoViewSet(ModelViewSet):
# Check permissions - allow owner or staff
if not (request.user == photo.uploaded_by or cast(Any, request.user).is_staff):
raise PermissionDenied(
"You can only modify your own photos or be an admin."
)
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
)
ParkMediaService().set_primary_photo(park_id=photo.park_id, photo_id=photo.id)
# Refresh the photo instance
photo.refresh_from_db()
@@ -290,7 +270,7 @@ class ParkPhotoViewSet(ModelViewSet):
return Response(
{
"message": "Photo set as primary successfully",
"detail": "Photo set as primary successfully",
"photo": serializer.data,
},
status=status.HTTP_200_OK,
@@ -337,7 +317,7 @@ class ParkPhotoViewSet(ModelViewSet):
if photo_ids is None or approve is None:
return Response(
{"error": "Missing required fields: photo_ids and/or approve."},
{"detail": "Missing required fields: photo_ids and/or approve."},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -354,7 +334,7 @@ class ParkPhotoViewSet(ModelViewSet):
return Response(
{
"message": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
"detail": f"Successfully {'approved' if approve else 'rejected'} {updated_count} photos",
"updated_count": updated_count,
},
status=status.HTTP_200_OK,
@@ -430,19 +410,14 @@ class ParkPhotoViewSet(ModelViewSet):
def set_primary_legacy(self, request, id=None):
"""Legacy set primary action for backwards compatibility."""
photo = self.get_object()
if not (
request.user == photo.uploaded_by
or request.user.has_perm("parks.change_parkphoto")
):
if not (request.user == photo.uploaded_by or request.user.has_perm("parks.change_parkphoto")):
return Response(
{"error": "You do not have permission to edit photos for this park."},
{"detail": "You do not have permission to edit photos for this park."},
status=status.HTTP_403_FORBIDDEN,
)
try:
ParkMediaService().set_primary_photo(
park_id=photo.park_id, photo_id=photo.id
)
return Response({"message": "Photo set as primary successfully."})
ParkMediaService().set_primary_photo(park_id=photo.park_id, photo_id=photo.id)
return Response({"detail": "Photo set as primary successfully."})
except (ValidationException, ValidationError) as e:
logger.warning(f"Validation error in set_primary_photo: {str(e)}")
return ErrorHandler.handle_api_error(
@@ -475,7 +450,7 @@ class ParkPhotoViewSet(ModelViewSet):
park_pk = self.kwargs.get("park_pk")
if not park_pk:
return Response(
{"error": "Park ID is required"},
{"detail": "Park ID is required"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -483,14 +458,14 @@ class ParkPhotoViewSet(ModelViewSet):
park = Park.objects.get(pk=park_pk) if str(park_pk).isdigit() else Park.objects.get(slug=park_pk)
except Park.DoesNotExist:
return Response(
{"error": "Park not found"},
{"detail": "Park not found"},
status=status.HTTP_404_NOT_FOUND,
)
cloudflare_image_id = request.data.get("cloudflare_image_id")
if not cloudflare_image_id:
return Response(
{"error": "cloudflare_image_id is required"},
{"detail": "cloudflare_image_id is required"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -515,18 +490,14 @@ class ParkPhotoViewSet(ModelViewSet):
# Try to find existing CloudflareImage record by cloudflare_id
cloudflare_image = None
try:
cloudflare_image = CloudflareImage.objects.get(
cloudflare_id=cloudflare_image_id
)
cloudflare_image = CloudflareImage.objects.get(cloudflare_id=cloudflare_image_id)
# Update existing record with latest data from Cloudflare
cloudflare_image.status = "uploaded"
cloudflare_image.uploaded_at = timezone.now()
cloudflare_image.metadata = image_data.get("meta", {})
# Extract variants from nested result structure
cloudflare_image.variants = image_data.get("result", {}).get(
"variants", []
)
cloudflare_image.variants = image_data.get("result", {}).get("variants", [])
cloudflare_image.cloudflare_metadata = image_data
cloudflare_image.width = image_data.get("width")
cloudflare_image.height = image_data.get("height")
@@ -540,8 +511,7 @@ class ParkPhotoViewSet(ModelViewSet):
user=request.user,
status="uploaded",
upload_url="", # Not needed for uploaded images
expires_at=timezone.now()
+ timezone.timedelta(days=365), # Set far future expiry
expires_at=timezone.now() + timezone.timedelta(days=365), # Set far future expiry
uploaded_at=timezone.now(),
metadata=image_data.get("meta", {}),
# Extract variants from nested result structure
@@ -567,9 +537,7 @@ class ParkPhotoViewSet(ModelViewSet):
# Handle primary photo logic if requested
if request.data.get("is_primary", False):
try:
ParkMediaService().set_primary_photo(
park_id=park.id, photo_id=photo.id
)
ParkMediaService().set_primary_photo(park_id=park.id, photo_id=photo.id)
except ServiceError as e:
logger.error(f"Error setting primary photo: {e}")
# Don't fail the entire operation, just log the error
@@ -624,12 +592,8 @@ class ParkPhotoViewSet(ModelViewSet):
OpenApiTypes.STR,
description="Filter by state (comma-separated for multiple)",
),
OpenApiParameter(
"opening_year_min", OpenApiTypes.INT, description="Minimum opening year"
),
OpenApiParameter(
"opening_year_max", OpenApiTypes.INT, description="Maximum opening year"
),
OpenApiParameter("opening_year_min", OpenApiTypes.INT, description="Minimum opening year"),
OpenApiParameter("opening_year_max", OpenApiTypes.INT, description="Maximum opening year"),
OpenApiParameter(
"size_min",
OpenApiTypes.NUMBER,
@@ -640,18 +604,10 @@ class ParkPhotoViewSet(ModelViewSet):
OpenApiTypes.NUMBER,
description="Maximum park size in acres",
),
OpenApiParameter(
"rating_min", OpenApiTypes.NUMBER, description="Minimum average rating"
),
OpenApiParameter(
"rating_max", OpenApiTypes.NUMBER, description="Maximum average rating"
),
OpenApiParameter(
"ride_count_min", OpenApiTypes.INT, description="Minimum ride count"
),
OpenApiParameter(
"ride_count_max", OpenApiTypes.INT, description="Maximum ride count"
),
OpenApiParameter("rating_min", OpenApiTypes.NUMBER, description="Minimum average rating"),
OpenApiParameter("rating_max", OpenApiTypes.NUMBER, description="Maximum average rating"),
OpenApiParameter("ride_count_min", OpenApiTypes.INT, description="Minimum ride count"),
OpenApiParameter("ride_count_max", OpenApiTypes.INT, description="Maximum ride count"),
OpenApiParameter(
"coaster_count_min",
OpenApiTypes.INT,
@@ -688,9 +644,7 @@ class ParkPhotoViewSet(ModelViewSet):
"properties": {
"parks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/HybridParkSerializer"
},
"items": {"$ref": "#/components/schemas/HybridParkSerializer"},
},
"total_count": {"type": "integer"},
"strategy": {
@@ -808,7 +762,7 @@ class HybridParkAPIView(APIView):
for param in int_params:
value = query_params.get(param)
if value:
try:
try: # noqa: SIM105
filters[param] = int(value)
except ValueError:
pass # Skip invalid integer values
@@ -818,7 +772,7 @@ class HybridParkAPIView(APIView):
for param in float_params:
value = query_params.get(param)
if value:
try:
try: # noqa: SIM105
filters[param] = float(value)
except ValueError:
pass # Skip invalid float values