Files
thrillwiki_django_no_react/backend/apps/api/v1/rides/serializers.py

605 lines
21 KiB
Python

"""
Ride media serializers for ThrillWiki API v1.
This module contains serializers for ride-specific media functionality.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_field,
extend_schema_serializer,
OpenApiExample,
)
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.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.url}/thumbnail",
"medium": f"{obj.image.url}/medium",
"large": f"{obj.image.url}/large",
"public": f"{obj.image.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",
"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",
"alt_text",
"photo_type",
"is_primary",
]
class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
"""Input serializer for updating ride photos."""
class Meta:
model = RidePhoto
fields = [
"caption",
"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.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.url
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",
"closing_date",
"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",
# 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",
# 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",
]