mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 12:55:17 -05:00
feat: add passkey authentication and enhance user preferences - Add passkey login security event type with fingerprint icon - Include request and site context in email confirmation for backend - Add user_id exact match filter to prevent incorrect user lookups - Enable PATCH method for updating user preferences via API - Add moderation_preferences support to user settings - Optimize ticket queries with select_related and prefetch_related This commit introduces passkey authentication tracking, improves user profile filtering accuracy, and extends the preferences API to support updates. Query optimizations reduce database hits for ticket listings.
1073 lines
39 KiB
Python
1073 lines
39 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()
|
|
|
|
# Metric unit conversions for frontend (duplicate of imperial fields)
|
|
coaster_height_meters = serializers.SerializerMethodField()
|
|
coaster_length_meters = serializers.SerializerMethodField()
|
|
coaster_speed_kmh = serializers.SerializerMethodField()
|
|
coaster_max_drop_meters = 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
|
|
|
|
# Metric conversions for frontend compatibility
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_height_meters(self, obj):
|
|
"""Convert coaster height from feet to meters."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.height_ft:
|
|
return round(float(obj.coaster_stats.height_ft) * 0.3048, 2)
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_length_meters(self, obj):
|
|
"""Convert coaster length from feet to meters."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.length_ft:
|
|
return round(float(obj.coaster_stats.length_ft) * 0.3048, 2)
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_speed_kmh(self, obj):
|
|
"""Convert coaster speed from mph to km/h."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.speed_mph:
|
|
return round(float(obj.coaster_stats.speed_mph) * 1.60934, 2)
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
def get_coaster_max_drop_meters(self, obj):
|
|
"""Convert coaster max drop from feet to meters."""
|
|
try:
|
|
if hasattr(obj, "coaster_stats") and obj.coaster_stats and obj.coaster_stats.max_drop_height_ft:
|
|
return round(float(obj.coaster_stats.max_drop_height_ft) * 0.3048, 2)
|
|
return None
|
|
except (AttributeError, TypeError):
|
|
return None
|
|
|
|
# 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",
|
|
]
|
|
|
|
|
|
class RideSubTypeSerializer(serializers.ModelSerializer):
|
|
"""Serializer for ride sub-types lookup table.
|
|
|
|
This serves the /rides/sub-types/ endpoint which the frontend
|
|
uses to populate sub-type dropdowns filtered by category.
|
|
"""
|
|
|
|
created_by = serializers.CharField(source="created_by.username", read_only=True, allow_null=True)
|
|
|
|
class Meta:
|
|
# Import here to avoid circular imports
|
|
from apps.rides.models import RideSubType
|
|
model = RideSubType
|
|
fields = [
|
|
"id",
|
|
"name",
|
|
"category",
|
|
"description",
|
|
"created_by",
|
|
"created_at",
|
|
]
|
|
read_only_fields = ["id", "created_at", "created_by"]
|
|
|
|
|