mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 09:25:18 -05:00
## FSM State Machine Fixes ### StateLog.by Field Capture - Modified TransitionMethodFactory to pass 'user' as 'by' kwarg to enable django-fsm-log's @fsm_log_by decorator to correctly capture the user who performed the transition - Applied fix to both escalate_transition and create_transition_method - Uses exec() to dynamically create transition functions with correct __name__ before decorators are applied, ensuring django-fsm's method registration works ### Cycle Validation Behavior - Changed validate_no_cycles() to return ValidationWarning instead of ValidationError - Cycles are now treated as warnings, not blocking errors, since cycles are often intentional in operational status FSMs (e.g., reopening after temporary closure) ### Ride Status Transitions - Added TEMPORARY_CLOSURE -> OPERATING transition (reopen after temporary closure) - Added SBNO -> OPERATING transition (revival - ride returns to operation) ## Field Parity ### Photo Models - Added 'photographer' field to RidePhoto and ParkPhoto models - Maps to frontend 'photographer_credit' field for full schema parity - Includes corresponding migrations for both apps ### Serializers - Added 'photographer' to RidePhotoSerializer and ParkPhotoSerializer read_only_fields
1000 lines
36 KiB
Python
1000 lines
36 KiB
Python
"""
|
|
Ride media serializers for ThrillWiki API v1.
|
|
|
|
This module contains serializers for ride-specific media functionality.
|
|
"""
|
|
|
|
from drf_spectacular.utils import (
|
|
OpenApiExample,
|
|
extend_schema_field,
|
|
extend_schema_serializer,
|
|
)
|
|
from rest_framework import serializers
|
|
|
|
from apps.rides.models import Ride, RidePhoto
|
|
|
|
|
|
@extend_schema_serializer(
|
|
examples=[
|
|
OpenApiExample(
|
|
name="Ride Photo with Cloudflare Images",
|
|
summary="Complete ride photo response",
|
|
description="Example response showing all fields including Cloudflare Images URLs and variants",
|
|
value={
|
|
"id": 123,
|
|
"image": "https://imagedelivery.net/account-hash/abc123def456/public",
|
|
"image_url": "https://imagedelivery.net/account-hash/abc123def456/public",
|
|
"image_variants": {
|
|
"thumbnail": "https://imagedelivery.net/account-hash/abc123def456/thumbnail",
|
|
"medium": "https://imagedelivery.net/account-hash/abc123def456/medium",
|
|
"large": "https://imagedelivery.net/account-hash/abc123def456/large",
|
|
"public": "https://imagedelivery.net/account-hash/abc123def456/public",
|
|
},
|
|
"caption": "Amazing roller coaster photo",
|
|
"alt_text": "Steel roller coaster with multiple inversions",
|
|
"is_primary": True,
|
|
"is_approved": True,
|
|
"photo_type": "exterior",
|
|
"created_at": "2023-01-01T12:00:00Z",
|
|
"updated_at": "2023-01-01T12:00:00Z",
|
|
"date_taken": "2023-01-01T10:00:00Z",
|
|
"uploaded_by_username": "photographer123",
|
|
"file_size": 2048576,
|
|
"dimensions": [1920, 1080],
|
|
"ride_slug": "steel-vengeance",
|
|
"ride_name": "Steel Vengeance",
|
|
"park_slug": "cedar-point",
|
|
"park_name": "Cedar Point",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
|
"""Output serializer for ride photos with Cloudflare Images support."""
|
|
|
|
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
|
|
|
|
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"))
|
|
def get_file_size(self, obj):
|
|
"""Get file size in bytes."""
|
|
return obj.file_size
|
|
|
|
@extend_schema_field(
|
|
serializers.ListField(
|
|
child=serializers.IntegerField(),
|
|
min_length=2,
|
|
max_length=2,
|
|
allow_null=True,
|
|
help_text="Image dimensions as [width, height] in pixels",
|
|
)
|
|
)
|
|
def get_dimensions(self, obj):
|
|
"""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.public_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 ride photos
|
|
variants = {
|
|
"thumbnail": f"{obj.image.public_url}/thumbnail",
|
|
"medium": f"{obj.image.public_url}/medium",
|
|
"large": f"{obj.image.public_url}/large",
|
|
"public": f"{obj.image.public_url}/public",
|
|
}
|
|
return variants
|
|
|
|
ride_slug = serializers.CharField(source="ride.slug", read_only=True)
|
|
ride_name = serializers.CharField(source="ride.name", read_only=True)
|
|
park_slug = serializers.CharField(source="ride.park.slug", read_only=True)
|
|
park_name = serializers.CharField(source="ride.park.name", read_only=True)
|
|
|
|
class Meta:
|
|
model = RidePhoto
|
|
fields = [
|
|
"id",
|
|
"image",
|
|
"image_url",
|
|
"image_variants",
|
|
"caption",
|
|
"photographer",
|
|
"alt_text",
|
|
"is_primary",
|
|
"is_approved",
|
|
"photo_type",
|
|
"created_at",
|
|
"updated_at",
|
|
"date_taken",
|
|
"uploaded_by_username",
|
|
"file_size",
|
|
"dimensions",
|
|
"ride_slug",
|
|
"ride_name",
|
|
"park_slug",
|
|
"park_name",
|
|
]
|
|
read_only_fields = [
|
|
"id",
|
|
"image_url",
|
|
"image_variants",
|
|
"created_at",
|
|
"updated_at",
|
|
"uploaded_by_username",
|
|
"file_size",
|
|
"dimensions",
|
|
"ride_slug",
|
|
"ride_name",
|
|
"park_slug",
|
|
"park_name",
|
|
]
|
|
|
|
|
|
class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
|
|
"""Input serializer for creating ride photos."""
|
|
|
|
class Meta:
|
|
model = RidePhoto
|
|
fields = [
|
|
"image",
|
|
"caption",
|
|
"photographer",
|
|
"alt_text",
|
|
"photo_type",
|
|
"is_primary",
|
|
]
|
|
|
|
|
|
class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
|
|
"""Input serializer for updating ride photos."""
|
|
|
|
class Meta:
|
|
model = RidePhoto
|
|
fields = [
|
|
"caption",
|
|
"photographer",
|
|
"alt_text",
|
|
"photo_type",
|
|
"is_primary",
|
|
]
|
|
|
|
|
|
class RidePhotoListOutputSerializer(serializers.ModelSerializer):
|
|
"""Simplified output serializer for ride photo lists."""
|
|
|
|
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
|
|
|
|
class Meta:
|
|
model = RidePhoto
|
|
fields = [
|
|
"id",
|
|
"image",
|
|
"caption",
|
|
"photo_type",
|
|
"is_primary",
|
|
"is_approved",
|
|
"created_at",
|
|
"uploaded_by_username",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class RidePhotoApprovalInputSerializer(serializers.Serializer):
|
|
"""Input serializer for photo approval operations."""
|
|
|
|
photo_ids = serializers.ListField(child=serializers.IntegerField(), help_text="List of photo IDs to approve")
|
|
approve = serializers.BooleanField(default=True, help_text="Whether to approve (True) or reject (False) the photos")
|
|
|
|
|
|
class RidePhotoStatsOutputSerializer(serializers.Serializer):
|
|
"""Output serializer for ride photo statistics."""
|
|
|
|
total_photos = serializers.IntegerField()
|
|
approved_photos = serializers.IntegerField()
|
|
pending_photos = serializers.IntegerField()
|
|
has_primary = serializers.BooleanField()
|
|
recent_uploads = serializers.IntegerField()
|
|
by_type = serializers.DictField(child=serializers.IntegerField(), help_text="Photo counts by type")
|
|
|
|
|
|
class RidePhotoTypeFilterSerializer(serializers.Serializer):
|
|
"""Serializer for filtering photos by type."""
|
|
|
|
photo_type = serializers.ChoiceField(
|
|
choices=[
|
|
("exterior", "Exterior View"),
|
|
("queue", "Queue Area"),
|
|
("station", "Station"),
|
|
("onride", "On-Ride"),
|
|
("construction", "Construction"),
|
|
("other", "Other"),
|
|
],
|
|
required=False,
|
|
help_text="Filter photos by type",
|
|
)
|
|
|
|
|
|
class RidePhotoSerializer(serializers.ModelSerializer):
|
|
"""Legacy serializer for backward compatibility."""
|
|
|
|
class Meta:
|
|
model = RidePhoto
|
|
fields = [
|
|
"id",
|
|
"image",
|
|
"caption",
|
|
"alt_text",
|
|
"is_primary",
|
|
"photo_type",
|
|
"uploaded_at",
|
|
"uploaded_by",
|
|
]
|
|
|
|
|
|
class HybridRideSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Enhanced serializer for hybrid filtering strategy.
|
|
Includes all filterable fields for client-side filtering.
|
|
"""
|
|
|
|
# Park fields
|
|
park_name = serializers.CharField(source="park.name", read_only=True)
|
|
park_slug = serializers.CharField(source="park.slug", read_only=True)
|
|
|
|
# Park location fields
|
|
park_city = serializers.SerializerMethodField()
|
|
park_state = serializers.SerializerMethodField()
|
|
park_country = serializers.SerializerMethodField()
|
|
|
|
# Park area fields
|
|
park_area_name = serializers.CharField(source="park_area.name", read_only=True, allow_null=True)
|
|
park_area_slug = serializers.CharField(source="park_area.slug", read_only=True, allow_null=True)
|
|
|
|
# Company fields
|
|
manufacturer_name = serializers.CharField(source="manufacturer.name", read_only=True, allow_null=True)
|
|
manufacturer_slug = serializers.CharField(source="manufacturer.slug", read_only=True, allow_null=True)
|
|
designer_name = serializers.CharField(source="designer.name", read_only=True, allow_null=True)
|
|
designer_slug = serializers.CharField(source="designer.slug", read_only=True, allow_null=True)
|
|
|
|
# Ride model fields
|
|
ride_model_name = serializers.CharField(source="ride_model.name", read_only=True, allow_null=True)
|
|
ride_model_slug = serializers.CharField(source="ride_model.slug", read_only=True, allow_null=True)
|
|
ride_model_category = serializers.CharField(source="ride_model.category", read_only=True, allow_null=True)
|
|
ride_model_manufacturer_name = serializers.CharField(
|
|
source="ride_model.manufacturer.name", read_only=True, allow_null=True
|
|
)
|
|
ride_model_manufacturer_slug = serializers.CharField(
|
|
source="ride_model.manufacturer.slug", read_only=True, allow_null=True
|
|
)
|
|
|
|
# Roller coaster stats fields
|
|
coaster_height_ft = serializers.SerializerMethodField()
|
|
coaster_length_ft = serializers.SerializerMethodField()
|
|
coaster_speed_mph = serializers.SerializerMethodField()
|
|
coaster_inversions = serializers.SerializerMethodField()
|
|
coaster_ride_time_seconds = serializers.SerializerMethodField()
|
|
coaster_track_type = serializers.SerializerMethodField()
|
|
coaster_track_material = serializers.SerializerMethodField()
|
|
coaster_roller_coaster_type = serializers.SerializerMethodField()
|
|
coaster_max_drop_height_ft = serializers.SerializerMethodField()
|
|
coaster_propulsion_system = serializers.SerializerMethodField()
|
|
coaster_train_style = serializers.SerializerMethodField()
|
|
coaster_trains_count = serializers.SerializerMethodField()
|
|
coaster_cars_per_train = serializers.SerializerMethodField()
|
|
coaster_seats_per_car = serializers.SerializerMethodField()
|
|
|
|
# Image URLs for display
|
|
banner_image_url = serializers.SerializerMethodField()
|
|
card_image_url = serializers.SerializerMethodField()
|
|
|
|
# Computed fields for filtering
|
|
opening_year = serializers.IntegerField(read_only=True)
|
|
search_text = serializers.CharField(read_only=True)
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_park_city(self, obj):
|
|
"""Get city from park location."""
|
|
try:
|
|
if obj.park and hasattr(obj.park, "location") and obj.park.location:
|
|
return obj.park.location.city
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_park_state(self, obj):
|
|
"""Get state from park location."""
|
|
try:
|
|
if obj.park and hasattr(obj.park, "location") and obj.park.location:
|
|
return obj.park.location.state
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_park_country(self, obj):
|
|
"""Get country from park location."""
|
|
try:
|
|
if obj.park and hasattr(obj.park, "location") and obj.park.location:
|
|
return obj.park.location.country
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_height_ft(self, obj):
|
|
"""Get roller coaster height."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return float(obj.coaster_stats.height_ft) if obj.coaster_stats.height_ft else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_length_ft(self, obj):
|
|
"""Get roller coaster length."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return float(obj.coaster_stats.length_ft) if obj.coaster_stats.length_ft else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_speed_mph(self, obj):
|
|
"""Get roller coaster speed."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return float(obj.coaster_stats.speed_mph) if obj.coaster_stats.speed_mph else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_coaster_inversions(self, obj):
|
|
"""Get roller coaster inversions."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.inversions
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_coaster_ride_time_seconds(self, obj):
|
|
"""Get roller coaster ride time."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.ride_time_seconds
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_coaster_track_type(self, obj):
|
|
"""Get roller coaster track type."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.track_type
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_coaster_track_material(self, obj):
|
|
"""Get roller coaster track material."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.track_material
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_coaster_roller_coaster_type(self, obj):
|
|
"""Get roller coaster type."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.roller_coaster_type
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_max_drop_height_ft(self, obj):
|
|
"""Get roller coaster max drop height."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return float(obj.coaster_stats.max_drop_height_ft) if obj.coaster_stats.max_drop_height_ft else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_coaster_propulsion_system(self, obj):
|
|
"""Get roller coaster propulsion system."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.propulsion_system
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_coaster_train_style(self, obj):
|
|
"""Get roller coaster train style."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.train_style
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_coaster_trains_count(self, obj):
|
|
"""Get roller coaster trains count."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.trains_count
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_coaster_cars_per_train(self, obj):
|
|
"""Get roller coaster cars per train."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.cars_per_train
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_coaster_seats_per_car(self, obj):
|
|
"""Get roller coaster seats per car."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats:
|
|
return obj.coaster_stats.seats_per_car
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
|
def get_banner_image_url(self, obj):
|
|
"""Get banner image URL."""
|
|
if obj.banner_image and obj.banner_image.image:
|
|
return obj.banner_image.image.public_url
|
|
return None
|
|
|
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
|
def get_card_image_url(self, obj):
|
|
"""Get card image URL."""
|
|
if obj.card_image and obj.card_image.image:
|
|
return obj.card_image.image.public_url
|
|
return None
|
|
|
|
# Computed property
|
|
is_closing = serializers.SerializerMethodField()
|
|
|
|
@extend_schema_field(serializers.BooleanField())
|
|
def get_is_closing(self, obj):
|
|
"""Check if ride has an announced closing date in the future."""
|
|
return obj.is_closing
|
|
|
|
# Water ride stats fields
|
|
water_wetness_level = serializers.SerializerMethodField()
|
|
water_splash_height_ft = serializers.SerializerMethodField()
|
|
water_has_splash_zone = serializers.SerializerMethodField()
|
|
water_boat_capacity = serializers.SerializerMethodField()
|
|
water_uses_flume = serializers.SerializerMethodField()
|
|
water_rapids_sections = serializers.SerializerMethodField()
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_water_wetness_level(self, obj):
|
|
try:
|
|
if hasattr(obj, "water_stats") and obj.water_stats:
|
|
return obj.water_stats.wetness_level
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_water_splash_height_ft(self, obj):
|
|
try:
|
|
if hasattr(obj, "water_stats") and obj.water_stats:
|
|
return float(obj.water_stats.splash_height_ft) if obj.water_stats.splash_height_ft else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_water_has_splash_zone(self, obj):
|
|
try:
|
|
if hasattr(obj, "water_stats") and obj.water_stats:
|
|
return obj.water_stats.has_splash_zone
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_water_boat_capacity(self, obj):
|
|
try:
|
|
if hasattr(obj, "water_stats") and obj.water_stats:
|
|
return obj.water_stats.boat_capacity
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_water_uses_flume(self, obj):
|
|
try:
|
|
if hasattr(obj, "water_stats") and obj.water_stats:
|
|
return obj.water_stats.uses_flume
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_water_rapids_sections(self, obj):
|
|
try:
|
|
if hasattr(obj, "water_stats") and obj.water_stats:
|
|
return obj.water_stats.rapids_sections
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
# Dark ride stats fields
|
|
dark_scene_count = serializers.SerializerMethodField()
|
|
dark_animatronic_count = serializers.SerializerMethodField()
|
|
dark_has_projection_technology = serializers.SerializerMethodField()
|
|
dark_is_interactive = serializers.SerializerMethodField()
|
|
dark_ride_system = serializers.SerializerMethodField()
|
|
dark_uses_practical_effects = serializers.SerializerMethodField()
|
|
dark_uses_motion_base = serializers.SerializerMethodField()
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_dark_scene_count(self, obj):
|
|
try:
|
|
if hasattr(obj, "dark_stats") and obj.dark_stats:
|
|
return obj.dark_stats.scene_count
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_dark_animatronic_count(self, obj):
|
|
try:
|
|
if hasattr(obj, "dark_stats") and obj.dark_stats:
|
|
return obj.dark_stats.animatronic_count
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_dark_has_projection_technology(self, obj):
|
|
try:
|
|
if hasattr(obj, "dark_stats") and obj.dark_stats:
|
|
return obj.dark_stats.has_projection_technology
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_dark_is_interactive(self, obj):
|
|
try:
|
|
if hasattr(obj, "dark_stats") and obj.dark_stats:
|
|
return obj.dark_stats.is_interactive
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_dark_ride_system(self, obj):
|
|
try:
|
|
if hasattr(obj, "dark_stats") and obj.dark_stats:
|
|
return obj.dark_stats.ride_system
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_dark_uses_practical_effects(self, obj):
|
|
try:
|
|
if hasattr(obj, "dark_stats") and obj.dark_stats:
|
|
return obj.dark_stats.uses_practical_effects
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_dark_uses_motion_base(self, obj):
|
|
try:
|
|
if hasattr(obj, "dark_stats") and obj.dark_stats:
|
|
return obj.dark_stats.uses_motion_base
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
# Flat ride stats fields
|
|
flat_max_height_ft = serializers.SerializerMethodField()
|
|
flat_rotation_speed_rpm = serializers.SerializerMethodField()
|
|
flat_swing_angle_degrees = serializers.SerializerMethodField()
|
|
flat_motion_type = serializers.SerializerMethodField()
|
|
flat_arm_count = serializers.SerializerMethodField()
|
|
flat_seats_per_gondola = serializers.SerializerMethodField()
|
|
flat_max_g_force = serializers.SerializerMethodField()
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_flat_max_height_ft(self, obj):
|
|
try:
|
|
if hasattr(obj, "flat_stats") and obj.flat_stats:
|
|
return float(obj.flat_stats.max_height_ft) if obj.flat_stats.max_height_ft else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_flat_rotation_speed_rpm(self, obj):
|
|
try:
|
|
if hasattr(obj, "flat_stats") and obj.flat_stats:
|
|
return float(obj.flat_stats.rotation_speed_rpm) if obj.flat_stats.rotation_speed_rpm else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_flat_swing_angle_degrees(self, obj):
|
|
try:
|
|
if hasattr(obj, "flat_stats") and obj.flat_stats:
|
|
return obj.flat_stats.swing_angle_degrees
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_flat_motion_type(self, obj):
|
|
try:
|
|
if hasattr(obj, "flat_stats") and obj.flat_stats:
|
|
return obj.flat_stats.motion_type
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_flat_arm_count(self, obj):
|
|
try:
|
|
if hasattr(obj, "flat_stats") and obj.flat_stats:
|
|
return obj.flat_stats.arm_count
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_flat_seats_per_gondola(self, obj):
|
|
try:
|
|
if hasattr(obj, "flat_stats") and obj.flat_stats:
|
|
return obj.flat_stats.seats_per_gondola
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_flat_max_g_force(self, obj):
|
|
try:
|
|
if hasattr(obj, "flat_stats") and obj.flat_stats:
|
|
return float(obj.flat_stats.max_g_force) if obj.flat_stats.max_g_force else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
# Kiddie ride stats fields
|
|
kiddie_min_age = serializers.SerializerMethodField()
|
|
kiddie_max_age = serializers.SerializerMethodField()
|
|
kiddie_educational_theme = serializers.SerializerMethodField()
|
|
kiddie_character_theme = serializers.SerializerMethodField()
|
|
kiddie_guardian_required = serializers.SerializerMethodField()
|
|
kiddie_adult_ride_along = serializers.SerializerMethodField()
|
|
kiddie_seats_per_vehicle = serializers.SerializerMethodField()
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_kiddie_min_age(self, obj):
|
|
try:
|
|
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
|
|
return obj.kiddie_stats.min_age
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_kiddie_max_age(self, obj):
|
|
try:
|
|
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
|
|
return obj.kiddie_stats.max_age
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_kiddie_educational_theme(self, obj):
|
|
try:
|
|
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
|
|
return obj.kiddie_stats.educational_theme or None
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_kiddie_character_theme(self, obj):
|
|
try:
|
|
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
|
|
return obj.kiddie_stats.character_theme or None
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_kiddie_guardian_required(self, obj):
|
|
try:
|
|
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
|
|
return obj.kiddie_stats.guardian_required
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_kiddie_adult_ride_along(self, obj):
|
|
try:
|
|
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
|
|
return obj.kiddie_stats.adult_ride_along
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_kiddie_seats_per_vehicle(self, obj):
|
|
try:
|
|
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
|
|
return obj.kiddie_stats.seats_per_vehicle
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
# Transportation stats fields
|
|
transport_type = serializers.SerializerMethodField()
|
|
transport_route_length_ft = serializers.SerializerMethodField()
|
|
transport_stations_count = serializers.SerializerMethodField()
|
|
transport_vehicle_capacity = serializers.SerializerMethodField()
|
|
transport_vehicles_count = serializers.SerializerMethodField()
|
|
transport_round_trip_duration_minutes = serializers.SerializerMethodField()
|
|
transport_scenic_highlights = serializers.SerializerMethodField()
|
|
transport_is_one_way = serializers.SerializerMethodField()
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_transport_type(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return obj.transport_stats.transport_type
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_transport_route_length_ft(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return float(obj.transport_stats.route_length_ft) if obj.transport_stats.route_length_ft else None
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_transport_stations_count(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return obj.transport_stats.stations_count
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_transport_vehicle_capacity(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return obj.transport_stats.vehicle_capacity
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_transport_vehicles_count(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return obj.transport_stats.vehicles_count
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.IntegerField(allow_null=True))
|
|
def get_transport_round_trip_duration_minutes(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return obj.transport_stats.round_trip_duration_minutes
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.CharField(allow_null=True))
|
|
def get_transport_scenic_highlights(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return obj.transport_stats.scenic_highlights or None
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
@extend_schema_field(serializers.BooleanField(allow_null=True))
|
|
def get_transport_is_one_way(self, obj):
|
|
try:
|
|
if hasattr(obj, "transport_stats") and obj.transport_stats:
|
|
return obj.transport_stats.is_one_way
|
|
return None
|
|
except AttributeError:
|
|
return None
|
|
|
|
class Meta:
|
|
model = Ride
|
|
fields = [
|
|
# Basic ride info
|
|
"id",
|
|
"name",
|
|
"slug",
|
|
"description",
|
|
"category",
|
|
"status",
|
|
"post_closing_status",
|
|
# Dates and computed fields
|
|
"opening_date",
|
|
"opening_date_precision",
|
|
"closing_date",
|
|
"closing_date_precision",
|
|
"is_closing",
|
|
"status_since",
|
|
"opening_year",
|
|
# Park fields
|
|
"park_name",
|
|
"park_slug",
|
|
"park_city",
|
|
"park_state",
|
|
"park_country",
|
|
# Park area fields
|
|
"park_area_name",
|
|
"park_area_slug",
|
|
# Company fields
|
|
"manufacturer_name",
|
|
"manufacturer_slug",
|
|
"designer_name",
|
|
"designer_slug",
|
|
# Ride model fields
|
|
"ride_model_name",
|
|
"ride_model_slug",
|
|
"ride_model_category",
|
|
"ride_model_manufacturer_name",
|
|
"ride_model_manufacturer_slug",
|
|
# Ride specifications
|
|
"min_height_in",
|
|
"max_height_in",
|
|
"capacity_per_hour",
|
|
"ride_duration_seconds",
|
|
"average_rating",
|
|
# Additional classification
|
|
"ride_sub_type",
|
|
"age_requirement",
|
|
# Roller coaster stats
|
|
"coaster_height_ft",
|
|
"coaster_length_ft",
|
|
"coaster_speed_mph",
|
|
"coaster_inversions",
|
|
"coaster_ride_time_seconds",
|
|
"coaster_track_type",
|
|
"coaster_track_material",
|
|
"coaster_roller_coaster_type",
|
|
"coaster_max_drop_height_ft",
|
|
"coaster_propulsion_system",
|
|
"coaster_train_style",
|
|
"coaster_trains_count",
|
|
"coaster_cars_per_train",
|
|
"coaster_seats_per_car",
|
|
# Water ride stats
|
|
"water_wetness_level",
|
|
"water_splash_height_ft",
|
|
"water_has_splash_zone",
|
|
"water_boat_capacity",
|
|
"water_uses_flume",
|
|
"water_rapids_sections",
|
|
# Dark ride stats
|
|
"dark_scene_count",
|
|
"dark_animatronic_count",
|
|
"dark_has_projection_technology",
|
|
"dark_is_interactive",
|
|
"dark_ride_system",
|
|
"dark_uses_practical_effects",
|
|
"dark_uses_motion_base",
|
|
# Flat ride stats
|
|
"flat_max_height_ft",
|
|
"flat_rotation_speed_rpm",
|
|
"flat_swing_angle_degrees",
|
|
"flat_motion_type",
|
|
"flat_arm_count",
|
|
"flat_seats_per_gondola",
|
|
"flat_max_g_force",
|
|
# Kiddie ride stats
|
|
"kiddie_min_age",
|
|
"kiddie_max_age",
|
|
"kiddie_educational_theme",
|
|
"kiddie_character_theme",
|
|
"kiddie_guardian_required",
|
|
"kiddie_adult_ride_along",
|
|
"kiddie_seats_per_vehicle",
|
|
# Transportation stats
|
|
"transport_type",
|
|
"transport_route_length_ft",
|
|
"transport_stations_count",
|
|
"transport_vehicle_capacity",
|
|
"transport_vehicles_count",
|
|
"transport_round_trip_duration_minutes",
|
|
"transport_scenic_highlights",
|
|
"transport_is_one_way",
|
|
# Images
|
|
"banner_image_url",
|
|
"card_image_url",
|
|
# URLs
|
|
"url",
|
|
"park_url",
|
|
# Computed fields for filtering
|
|
"search_text",
|
|
# Metadata
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class RideSerializer(serializers.ModelSerializer):
|
|
"""Serializer for the Ride model."""
|
|
|
|
class Meta:
|
|
model = Ride
|
|
fields = [
|
|
"id",
|
|
"name",
|
|
"slug",
|
|
"park",
|
|
"manufacturer",
|
|
"designer",
|
|
"category",
|
|
"status",
|
|
"opening_date",
|
|
"closing_date",
|
|
]
|