Files
thrillwiki_django_no_react/backend/apps/api/v1/serializers/parks.py
pacnpal ac745cc541 ok
2025-08-28 23:20:09 -04:00

678 lines
24 KiB
Python

"""
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 config.django import base as settings
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()
# URL
url = serializers.SerializerMethodField()
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
@extend_schema_field(serializers.URLField())
def get_url(self, obj) -> str:
"""Generate the frontend URL for this park."""
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.slug}/"
@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()
# URL
url = serializers.SerializerMethodField()
@extend_schema_field(serializers.URLField())
def get_url(self, obj) -> str:
"""Generate the frontend URL for this park."""
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.slug}/"
@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()