mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 13:11:08 -05:00
- Updated ParkDetailOutputSerializer to utilize MediaURLService for generating Cloudflare URLs and friendly URLs for park photos. - Added support for multiple lookup methods (ID and slug) in the park detail endpoint. - Improved documentation for the park detail endpoint, including request properties and response structure. - Created MediaURLService for generating SEO-friendly URLs and handling Cloudflare image URLs. - Comprehensive updates to frontend documentation to reflect new endpoint capabilities and usage examples. - Added detailed park detail endpoint documentation, including request and response structures, field descriptions, and usage examples.
722 lines
28 KiB
Python
722 lines
28 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
|
|
from apps.core.services.media_url_service import MediaURLService
|
|
|
|
|
|
# === 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.pk,
|
|
"image_url": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
|
|
"image_variants": {
|
|
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "thumbnail"),
|
|
"medium": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "medium"),
|
|
"large": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "large"),
|
|
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
|
|
},
|
|
"friendly_urls": {
|
|
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
|
|
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
|
|
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
|
|
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "public"),
|
|
},
|
|
"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.pk,
|
|
"image_url": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
|
|
"image_variants": {
|
|
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "thumbnail"),
|
|
"medium": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "medium"),
|
|
"large": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "large"),
|
|
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
|
|
},
|
|
"friendly_urls": {
|
|
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
|
|
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
|
|
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
|
|
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "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.pk,
|
|
"image_url": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "public"),
|
|
"image_variants": {
|
|
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "thumbnail"),
|
|
"medium": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "medium"),
|
|
"large": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "large"),
|
|
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "public"),
|
|
},
|
|
"friendly_urls": {
|
|
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "thumbnail"),
|
|
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "medium"),
|
|
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "large"),
|
|
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "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.pk,
|
|
"image_url": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
|
|
"image_variants": {
|
|
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "thumbnail"),
|
|
"medium": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "medium"),
|
|
"large": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "large"),
|
|
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
|
|
},
|
|
"friendly_urls": {
|
|
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
|
|
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
|
|
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
|
|
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "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.pk,
|
|
"image_url": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "public"),
|
|
"image_variants": {
|
|
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "thumbnail"),
|
|
"medium": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "medium"),
|
|
"large": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "large"),
|
|
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "public"),
|
|
},
|
|
"friendly_urls": {
|
|
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "thumbnail"),
|
|
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "medium"),
|
|
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "large"),
|
|
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "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.pk,
|
|
"image_url": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
|
|
"image_variants": {
|
|
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "thumbnail"),
|
|
"medium": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "medium"),
|
|
"large": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "large"),
|
|
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
|
|
},
|
|
"friendly_urls": {
|
|
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
|
|
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
|
|
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
|
|
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "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:
|
|
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:
|
|
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()
|