Revert "update"

This reverts commit 75cc618c2b.
This commit is contained in:
pacnpal
2025-09-21 20:11:00 -04:00
parent 75cc618c2b
commit 540f40e689
610 changed files with 4812 additions and 1715 deletions

View File

@@ -1,728 +0,0 @@
"""
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
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === 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 = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
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 = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
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()