""" Parks domain serializers for ThrillWiki API v1. This module contains all serializers related to parks, park areas, park locations, and park search functionality. """ from rest_framework import serializers from drf_spectacular.utils import ( extend_schema_serializer, extend_schema_field, OpenApiExample, ) from .shared import LocationOutputSerializer, CompanyOutputSerializer, ModelChoices # === PARK SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Park List Example", summary="Example park list response", description="A typical park in the list view", value={ "id": 1, "name": "Cedar Point", "slug": "cedar-point", "status": "OPERATING", "description": "America's Roller Coast", "average_rating": 4.5, "coaster_count": 17, "ride_count": 70, "location": { "city": "Sandusky", "state": "Ohio", "country": "United States", }, "operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"}, }, ) ] ) class ParkListOutputSerializer(serializers.Serializer): """Output serializer for park list view.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() status = serializers.CharField() description = serializers.CharField() # Statistics average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) coaster_count = serializers.IntegerField(allow_null=True) ride_count = serializers.IntegerField(allow_null=True) # Location (simplified for list view) location = LocationOutputSerializer(allow_null=True) # Operator info operator = CompanyOutputSerializer() # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() @extend_schema_serializer( examples=[ OpenApiExample( "Park Detail Example", summary="Example park detail response", description="A complete park detail response", value={ "id": 1, "name": "Cedar Point", "slug": "cedar-point", "status": "OPERATING", "description": "America's Roller Coast", "opening_date": "1870-01-01", "website": "https://cedarpoint.com", "size_acres": 364.0, "average_rating": 4.5, "coaster_count": 17, "ride_count": 70, "location": { "latitude": 41.4793, "longitude": -82.6833, "city": "Sandusky", "state": "Ohio", "country": "United States", }, "operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"}, "photos": [ { "id": 456, "image_url": "https://imagedelivery.net/account-hash/def789ghi012/public", "image_variants": { "thumbnail": "https://imagedelivery.net/account-hash/def789ghi012/thumbnail", "medium": "https://imagedelivery.net/account-hash/def789ghi012/medium", "large": "https://imagedelivery.net/account-hash/def789ghi012/large", "public": "https://imagedelivery.net/account-hash/def789ghi012/public" }, "caption": "Beautiful park entrance", "is_primary": True } ], "primary_photo": { "id": 456, "image_url": "https://imagedelivery.net/account-hash/def789ghi012/public", "image_variants": { "thumbnail": "https://imagedelivery.net/account-hash/def789ghi012/thumbnail", "medium": "https://imagedelivery.net/account-hash/def789ghi012/medium", "large": "https://imagedelivery.net/account-hash/def789ghi012/large", "public": "https://imagedelivery.net/account-hash/def789ghi012/public" }, "caption": "Beautiful park entrance" } }, ) ] ) class ParkDetailOutputSerializer(serializers.Serializer): """Output serializer for park detail view.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() status = serializers.CharField() description = serializers.CharField() # Details opening_date = serializers.DateField(allow_null=True) closing_date = serializers.DateField(allow_null=True) operating_season = serializers.CharField() size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, allow_null=True ) website = serializers.URLField() # Statistics average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) coaster_count = serializers.IntegerField(allow_null=True) ride_count = serializers.IntegerField(allow_null=True) # Location (full details) location = LocationOutputSerializer(allow_null=True) # Companies operator = CompanyOutputSerializer() property_owner = CompanyOutputSerializer(allow_null=True) # Areas areas = serializers.SerializerMethodField() # Photos photos = serializers.SerializerMethodField() primary_photo = serializers.SerializerMethodField() banner_image = serializers.SerializerMethodField() card_image = serializers.SerializerMethodField() @extend_schema_field(serializers.ListField(child=serializers.DictField())) def get_areas(self, obj): """Get simplified area information.""" if hasattr(obj, "areas"): return [ { "id": area.id, "name": area.name, "slug": area.slug, "description": area.description, } for area in obj.areas.all() ] return [] @extend_schema_field(serializers.ListField(child=serializers.DictField())) def get_photos(self, obj): """Get all approved photos for this park.""" from apps.parks.models import ParkPhoto photos = ParkPhoto.objects.filter( park=obj, is_approved=True ).order_by('-is_primary', '-created_at')[:10] # Limit to 10 photos return [ { "id": photo.id, "image_url": photo.image.url if photo.image else None, "image_variants": { "thumbnail": f"{photo.image.url}/thumbnail" if photo.image else None, "medium": f"{photo.image.url}/medium" if photo.image else None, "large": f"{photo.image.url}/large" if photo.image else None, "public": f"{photo.image.url}/public" if photo.image else None, } if photo.image else {}, "caption": photo.caption, "alt_text": photo.alt_text, "is_primary": photo.is_primary, } for photo in photos ] @extend_schema_field(serializers.DictField(allow_null=True)) def get_primary_photo(self, obj): """Get the primary photo for this park.""" from apps.parks.models import ParkPhoto try: photo = ParkPhoto.objects.filter( park=obj, is_primary=True, is_approved=True ).first() if photo and photo.image: return { "id": photo.id, "image_url": photo.image.url, "image_variants": { "thumbnail": f"{photo.image.url}/thumbnail", "medium": f"{photo.image.url}/medium", "large": f"{photo.image.url}/large", "public": f"{photo.image.url}/public", }, "caption": photo.caption, "alt_text": photo.alt_text, } except Exception: pass return None @extend_schema_field(serializers.DictField(allow_null=True)) def get_banner_image(self, obj): """Get the banner image for this park with fallback to latest photo.""" # First try the explicitly set banner image if obj.banner_image and obj.banner_image.image: return { "id": obj.banner_image.id, "image_url": obj.banner_image.image.url, "image_variants": { "thumbnail": f"{obj.banner_image.image.url}/thumbnail", "medium": f"{obj.banner_image.image.url}/medium", "large": f"{obj.banner_image.image.url}/large", "public": f"{obj.banner_image.image.url}/public", }, "caption": obj.banner_image.caption, "alt_text": obj.banner_image.alt_text, } # Fallback to latest approved photo from apps.parks.models import ParkPhoto try: latest_photo = ParkPhoto.objects.filter( park=obj, is_approved=True, image__isnull=False ).order_by('-created_at').first() if latest_photo and latest_photo.image: return { "id": latest_photo.id, "image_url": latest_photo.image.url, "image_variants": { "thumbnail": f"{latest_photo.image.url}/thumbnail", "medium": f"{latest_photo.image.url}/medium", "large": f"{latest_photo.image.url}/large", "public": f"{latest_photo.image.url}/public", }, "caption": latest_photo.caption, "alt_text": latest_photo.alt_text, "is_fallback": True, } except Exception: pass return None @extend_schema_field(serializers.DictField(allow_null=True)) def get_card_image(self, obj): """Get the card image for this park with fallback to latest photo.""" # First try the explicitly set card image if obj.card_image and obj.card_image.image: return { "id": obj.card_image.id, "image_url": obj.card_image.image.url, "image_variants": { "thumbnail": f"{obj.card_image.image.url}/thumbnail", "medium": f"{obj.card_image.image.url}/medium", "large": f"{obj.card_image.image.url}/large", "public": f"{obj.card_image.image.url}/public", }, "caption": obj.card_image.caption, "alt_text": obj.card_image.alt_text, } # Fallback to latest approved photo from apps.parks.models import ParkPhoto try: latest_photo = ParkPhoto.objects.filter( park=obj, is_approved=True, image__isnull=False ).order_by('-created_at').first() if latest_photo and latest_photo.image: return { "id": latest_photo.id, "image_url": latest_photo.image.url, "image_variants": { "thumbnail": f"{latest_photo.image.url}/thumbnail", "medium": f"{latest_photo.image.url}/medium", "large": f"{latest_photo.image.url}/large", "public": f"{latest_photo.image.url}/public", }, "caption": latest_photo.caption, "alt_text": latest_photo.alt_text, "is_fallback": True, } except Exception: pass return None # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() class ParkImageSettingsInputSerializer(serializers.Serializer): """Input serializer for setting park banner and card images.""" banner_image_id = serializers.IntegerField(required=False, allow_null=True) card_image_id = serializers.IntegerField(required=False, allow_null=True) def validate_banner_image_id(self, value): """Validate that the banner image belongs to the same park.""" if value is not None: from apps.parks.models import ParkPhoto try: photo = ParkPhoto.objects.get(id=value) # The park will be validated in the view return value except ParkPhoto.DoesNotExist: raise serializers.ValidationError("Photo not found") return value def validate_card_image_id(self, value): """Validate that the card image belongs to the same park.""" if value is not None: from apps.parks.models import ParkPhoto try: photo = ParkPhoto.objects.get(id=value) # The park will be validated in the view return value except ParkPhoto.DoesNotExist: raise serializers.ValidationError("Photo not found") return value class ParkCreateInputSerializer(serializers.Serializer): """Input serializer for creating parks.""" name = serializers.CharField(max_length=255) description = serializers.CharField(allow_blank=True, default="") status = serializers.ChoiceField( choices=ModelChoices.get_park_status_choices(), default="OPERATING" ) # Optional details opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) operating_season = serializers.CharField( max_length=255, required=False, allow_blank=True ) size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, allow_null=True ) website = serializers.URLField(required=False, allow_blank=True) # Required operator operator_id = serializers.IntegerField() # Optional property owner property_owner_id = serializers.IntegerField(required=False, allow_null=True) def validate(self, attrs): """Cross-field validation.""" opening_date = attrs.get("opening_date") closing_date = attrs.get("closing_date") if opening_date and closing_date and closing_date < opening_date: raise serializers.ValidationError( "Closing date cannot be before opening date" ) return attrs class ParkUpdateInputSerializer(serializers.Serializer): """Input serializer for updating parks.""" name = serializers.CharField(max_length=255, required=False) description = serializers.CharField(allow_blank=True, required=False) status = serializers.ChoiceField( choices=ModelChoices.get_park_status_choices(), required=False ) # Optional details opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) operating_season = serializers.CharField( max_length=255, required=False, allow_blank=True ) size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, allow_null=True ) website = serializers.URLField(required=False, allow_blank=True) # Companies operator_id = serializers.IntegerField(required=False) property_owner_id = serializers.IntegerField(required=False, allow_null=True) def validate(self, attrs): """Cross-field validation.""" opening_date = attrs.get("opening_date") closing_date = attrs.get("closing_date") if opening_date and closing_date and closing_date < opening_date: raise serializers.ValidationError( "Closing date cannot be before opening date" ) return attrs class ParkFilterInputSerializer(serializers.Serializer): """Input serializer for park filtering and search.""" # Search search = serializers.CharField(required=False, allow_blank=True) # Status filter status = serializers.MultipleChoiceField( choices=[], required=False, # Choices set dynamically ) # Location filters country = serializers.CharField(required=False, allow_blank=True) state = serializers.CharField(required=False, allow_blank=True) city = serializers.CharField(required=False, allow_blank=True) # Rating filter min_rating = serializers.DecimalField( max_digits=3, decimal_places=2, required=False, min_value=1, max_value=10, ) # Size filter min_size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, min_value=0 ) max_size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, min_value=0 ) # Company filters operator_id = serializers.IntegerField(required=False) property_owner_id = serializers.IntegerField(required=False) # Ordering ordering = serializers.ChoiceField( choices=[ "name", "-name", "opening_date", "-opening_date", "average_rating", "-average_rating", "coaster_count", "-coaster_count", "created_at", "-created_at", ], required=False, default="name", ) # === PARK AREA SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Park Area Example", summary="Example park area response", description="A themed area within a park", value={ "id": 1, "name": "Tomorrowland", "slug": "tomorrowland", "description": "A futuristic themed area", "park": {"id": 1, "name": "Magic Kingdom", "slug": "magic-kingdom"}, "opening_date": "1971-10-01", "closing_date": None, }, ) ] ) class ParkAreaDetailOutputSerializer(serializers.Serializer): """Output serializer for park areas.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() description = serializers.CharField() opening_date = serializers.DateField(allow_null=True) closing_date = serializers.DateField(allow_null=True) # Park info park = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_park(self, obj) -> dict: return { "id": obj.park.id, "name": obj.park.name, "slug": obj.park.slug, } class ParkAreaCreateInputSerializer(serializers.Serializer): """Input serializer for creating park areas.""" name = serializers.CharField(max_length=255) description = serializers.CharField(allow_blank=True, default="") park_id = serializers.IntegerField() opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) def validate(self, attrs): """Cross-field validation.""" opening_date = attrs.get("opening_date") closing_date = attrs.get("closing_date") if opening_date and closing_date and closing_date < opening_date: raise serializers.ValidationError( "Closing date cannot be before opening date" ) return attrs class ParkAreaUpdateInputSerializer(serializers.Serializer): """Input serializer for updating park areas.""" name = serializers.CharField(max_length=255, required=False) description = serializers.CharField(allow_blank=True, required=False) opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) def validate(self, attrs): """Cross-field validation.""" opening_date = attrs.get("opening_date") closing_date = attrs.get("closing_date") if opening_date and closing_date and closing_date < opening_date: raise serializers.ValidationError( "Closing date cannot be before opening date" ) return attrs # === PARK LOCATION SERIALIZERS === class ParkLocationOutputSerializer(serializers.Serializer): """Output serializer for park locations.""" id = serializers.IntegerField() latitude = serializers.FloatField(allow_null=True) longitude = serializers.FloatField(allow_null=True) address = serializers.CharField() city = serializers.CharField() state = serializers.CharField() country = serializers.CharField() postal_code = serializers.CharField() formatted_address = serializers.CharField() # Park info park = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_park(self, obj) -> dict: return { "id": obj.park.id, "name": obj.park.name, "slug": obj.park.slug, } class ParkLocationCreateInputSerializer(serializers.Serializer): """Input serializer for creating park locations.""" park_id = serializers.IntegerField() latitude = serializers.FloatField(required=False, allow_null=True) longitude = serializers.FloatField(required=False, allow_null=True) address = serializers.CharField(max_length=255, allow_blank=True, default="") city = serializers.CharField(max_length=100) state = serializers.CharField(max_length=100) country = serializers.CharField(max_length=100) postal_code = serializers.CharField(max_length=20, allow_blank=True, default="") class ParkLocationUpdateInputSerializer(serializers.Serializer): """Input serializer for updating park locations.""" latitude = serializers.FloatField(required=False, allow_null=True) longitude = serializers.FloatField(required=False, allow_null=True) address = serializers.CharField(max_length=255, allow_blank=True, required=False) city = serializers.CharField(max_length=100, required=False) state = serializers.CharField(max_length=100, required=False) country = serializers.CharField(max_length=100, required=False) postal_code = serializers.CharField(max_length=20, allow_blank=True, required=False) # === PARKS SEARCH SERIALIZERS === class ParkSuggestionSerializer(serializers.Serializer): """Serializer for park search suggestions.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() location = serializers.CharField() status = serializers.CharField() coaster_count = serializers.IntegerField() class ParkSuggestionOutputSerializer(serializers.Serializer): """Output serializer for park suggestions.""" results = ParkSuggestionSerializer(many=True) query = serializers.CharField() count = serializers.IntegerField()