mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:31:08 -05:00
feat(rides): populate slugs for existing RideModel records and ensure uniqueness
- Added migration 0011 to populate unique slugs for existing RideModel records based on manufacturer and model names. - Implemented logic to ensure slug uniqueness during population. - Added reverse migration to clear slugs if needed. feat(rides): enforce unique slugs for RideModel - Created migration 0012 to alter the slug field in RideModel to be unique. - Updated the slug field to include help text and a maximum length of 255 characters. docs: integrate Cloudflare Images into rides and parks models - Updated RidePhoto and ParkPhoto models to use CloudflareImagesField for image storage. - Enhanced API serializers for rides and parks to support Cloudflare Images, including new fields for image URLs and variants. - Provided comprehensive OpenAPI schema metadata for new fields. - Documented database migrations for the integration. - Detailed configuration settings for Cloudflare Images. - Updated API response formats to include Cloudflare Images URLs and variants. - Added examples for uploading photos via API and outlined testing procedures.
This commit is contained in:
@@ -16,7 +16,8 @@ from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
from rest_framework.decorators import action
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
@@ -48,6 +49,7 @@ try:
|
||||
ParkDetailOutputSerializer,
|
||||
ParkCreateInputSerializer,
|
||||
ParkUpdateInputSerializer,
|
||||
ParkImageSettingsInputSerializer,
|
||||
)
|
||||
|
||||
SERIALIZERS_AVAILABLE = True
|
||||
@@ -414,3 +416,51 @@ class ParkSearchSuggestionsAPIView(APIView):
|
||||
{"suggestion": f"{q} Amusement Park"},
|
||||
]
|
||||
return Response(fallback)
|
||||
|
||||
|
||||
# --- Park image settings ---------------------------------------------------
|
||||
@extend_schema(
|
||||
summary="Set park banner and card images",
|
||||
description="Set banner_image and card_image for a park from existing park photos",
|
||||
request=("ParkImageSettingsInputSerializer" if SERIALIZERS_AVAILABLE else OpenApiTypes.OBJECT),
|
||||
responses={
|
||||
200: ("ParkDetailOutputSerializer" if SERIALIZERS_AVAILABLE else OpenApiTypes.OBJECT),
|
||||
400: OpenApiTypes.OBJECT,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Parks"],
|
||||
)
|
||||
class ParkImageSettingsAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def _get_park_or_404(self, pk: int) -> Any:
|
||||
if not MODELS_AVAILABLE:
|
||||
raise NotFound("Park models not available")
|
||||
try:
|
||||
return Park.objects.get(pk=pk) # type: ignore
|
||||
except Park.DoesNotExist: # type: ignore
|
||||
raise NotFound("Park not found")
|
||||
|
||||
def patch(self, request: Request, pk: int) -> Response:
|
||||
"""Set banner and card images for the park."""
|
||||
if not SERIALIZERS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Park image settings serializers not available."},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
park = self._get_park_or_404(pk)
|
||||
|
||||
serializer = ParkImageSettingsInputSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Update the park with the validated data
|
||||
for field, value in serializer.validated_data.items():
|
||||
setattr(park, field, value)
|
||||
|
||||
park.save()
|
||||
|
||||
# Return updated park data
|
||||
output_serializer = ParkDetailOutputSerializer(
|
||||
park, context={"request": request})
|
||||
return Response(output_serializer.data)
|
||||
|
||||
@@ -6,12 +6,44 @@ Enhanced from rogue implementation to maintain full feature parity.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer, OpenApiExample
|
||||
from apps.parks.models import Park, ParkPhoto
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name='Park Photo with Cloudflare Images',
|
||||
summary='Complete park photo response',
|
||||
description='Example response showing all fields including Cloudflare Images URLs and variants',
|
||||
value={
|
||||
'id': 456,
|
||||
'image': 'https://imagedelivery.net/account-hash/def456ghi789/public',
|
||||
'image_url': 'https://imagedelivery.net/account-hash/def456ghi789/public',
|
||||
'image_variants': {
|
||||
'thumbnail': 'https://imagedelivery.net/account-hash/def456ghi789/thumbnail',
|
||||
'medium': 'https://imagedelivery.net/account-hash/def456ghi789/medium',
|
||||
'large': 'https://imagedelivery.net/account-hash/def456ghi789/large',
|
||||
'public': 'https://imagedelivery.net/account-hash/def456ghi789/public'
|
||||
},
|
||||
'caption': 'Beautiful park entrance',
|
||||
'alt_text': 'Main entrance gate with decorative archway',
|
||||
'is_primary': True,
|
||||
'is_approved': True,
|
||||
'created_at': '2023-01-01T12:00:00Z',
|
||||
'updated_at': '2023-01-01T12:00:00Z',
|
||||
'date_taken': '2023-01-01T11:00:00Z',
|
||||
'uploaded_by_username': 'parkfan456',
|
||||
'file_size': 1536000,
|
||||
'dimensions': [1600, 900],
|
||||
'park_slug': 'cedar-point',
|
||||
'park_name': 'Cedar Point'
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
class ParkPhotoOutputSerializer(serializers.ModelSerializer):
|
||||
"""Enhanced output serializer for park photos with rich field structure."""
|
||||
"""Enhanced output serializer for park photos with Cloudflare Images support."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source="uploaded_by.username", read_only=True
|
||||
@@ -19,6 +51,8 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer):
|
||||
|
||||
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")
|
||||
@@ -40,6 +74,38 @@ 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
|
||||
)
|
||||
)
|
||||
def get_image_url(self, obj):
|
||||
"""Get the full Cloudflare Images URL."""
|
||||
if obj.image:
|
||||
return obj.image.url
|
||||
return None
|
||||
|
||||
@extend_schema_field(
|
||||
serializers.DictField(
|
||||
child=serializers.URLField(),
|
||||
help_text="Available Cloudflare Images variants with their URLs"
|
||||
)
|
||||
)
|
||||
def get_image_variants(self, obj):
|
||||
"""Get available image variants from Cloudflare Images."""
|
||||
if not obj.image:
|
||||
return {}
|
||||
|
||||
# Common variants for park photos
|
||||
variants = {
|
||||
'thumbnail': f"{obj.image.url}/thumbnail",
|
||||
'medium': f"{obj.image.url}/medium",
|
||||
'large': f"{obj.image.url}/large",
|
||||
'public': f"{obj.image.url}/public"
|
||||
}
|
||||
return variants
|
||||
|
||||
park_slug = serializers.CharField(source="park.slug", read_only=True)
|
||||
park_name = serializers.CharField(source="park.name", read_only=True)
|
||||
|
||||
@@ -48,6 +114,8 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer):
|
||||
fields = [
|
||||
"id",
|
||||
"image",
|
||||
"image_url",
|
||||
"image_variants",
|
||||
"caption",
|
||||
"alt_text",
|
||||
"is_primary",
|
||||
@@ -63,6 +131,8 @@ class ParkPhotoOutputSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"image_url",
|
||||
"image_variants",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"uploaded_by_username",
|
||||
|
||||
@@ -15,12 +15,13 @@ from .park_views import (
|
||||
FilterOptionsAPIView,
|
||||
CompanySearchAPIView,
|
||||
ParkSearchSuggestionsAPIView,
|
||||
ParkImageSettingsAPIView,
|
||||
)
|
||||
from .views import ParkPhotoViewSet
|
||||
|
||||
# Create router for nested photo endpoints
|
||||
router = DefaultRouter()
|
||||
router.register(r"photos", ParkPhotoViewSet, basename="park-photo")
|
||||
router.register(r"", ParkPhotoViewSet, basename="park-photo")
|
||||
|
||||
app_name = "api_v1_parks"
|
||||
|
||||
@@ -42,6 +43,8 @@ urlpatterns = [
|
||||
),
|
||||
# Detail and action endpoints
|
||||
path("<int:pk>/", ParkDetailAPIView.as_view(), name="park-detail"),
|
||||
# Park image settings endpoint
|
||||
path("<int:pk>/image-settings/", ParkImageSettingsAPIView.as_view(), name="park-image-settings"),
|
||||
# Park photo endpoints - domain-specific photo management
|
||||
path("<int:park_pk>/photos/", include(router.urls)),
|
||||
]
|
||||
|
||||
@@ -141,6 +141,12 @@ class ParkPhotoViewSet(ModelViewSet):
|
||||
park_id = self.kwargs.get("park_pk")
|
||||
if not park_id:
|
||||
raise ValidationError("Park ID is required")
|
||||
|
||||
try:
|
||||
park = Park.objects.get(pk=park_id)
|
||||
except Park.DoesNotExist:
|
||||
raise ValidationError("Park not found")
|
||||
|
||||
try:
|
||||
# Use the service to create the photo with proper business logic
|
||||
service = cast(Any, ParkMediaService())
|
||||
|
||||
Reference in New Issue
Block a user