mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:31:07 -05:00
feat(rides): populate slugs for existing RideModel records and ensure uniqueness
- Added migration 0011 to populate unique slugs for existing RideModel records based on manufacturer and model names. - Implemented logic to ensure slug uniqueness during population. - Added reverse migration to clear slugs if needed. feat(rides): enforce unique slugs for RideModel - Created migration 0012 to alter the slug field in RideModel to be unique. - Updated the slug field to include help text and a maximum length of 255 characters. docs: integrate Cloudflare Images into rides and parks models - Updated RidePhoto and ParkPhoto models to use CloudflareImagesField for image storage. - Enhanced API serializers for rides and parks to support Cloudflare Images, including new fields for image URLs and variants. - Provided comprehensive OpenAPI schema metadata for new fields. - Documented database migrations for the integration. - Detailed configuration settings for Cloudflare Images. - Updated API response formats to include Cloudflare Images URLs and variants. - Added examples for uploading photos via API and outlined testing procedures.
This commit is contained in:
@@ -96,6 +96,31 @@ class ParkListOutputSerializer(serializers.Serializer):
|
||||
"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"
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -135,6 +160,12 @@ class ParkDetailOutputSerializer(serializers.Serializer):
|
||||
# Areas
|
||||
areas = serializers.SerializerMethodField()
|
||||
|
||||
# Photos
|
||||
photos = serializers.SerializerMethodField()
|
||||
primary_photo = serializers.SerializerMethodField()
|
||||
banner_image = serializers.SerializerMethodField()
|
||||
card_image = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
|
||||
def get_areas(self, obj):
|
||||
"""Get simplified area information."""
|
||||
@@ -150,11 +181,191 @@ class ParkDetailOutputSerializer(serializers.Serializer):
|
||||
]
|
||||
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.id,
|
||||
"image_url": photo.image.url if photo.image else None,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{photo.image.url}/thumbnail" if photo.image else None,
|
||||
"medium": f"{photo.image.url}/medium" if photo.image else None,
|
||||
"large": f"{photo.image.url}/large" if photo.image else None,
|
||||
"public": f"{photo.image.url}/public" if photo.image else None,
|
||||
} if photo.image else {},
|
||||
"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.id,
|
||||
"image_url": photo.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{photo.image.url}/thumbnail",
|
||||
"medium": f"{photo.image.url}/medium",
|
||||
"large": f"{photo.image.url}/large",
|
||||
"public": f"{photo.image.url}/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.id,
|
||||
"image_url": obj.banner_image.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{obj.banner_image.image.url}/thumbnail",
|
||||
"medium": f"{obj.banner_image.image.url}/medium",
|
||||
"large": f"{obj.banner_image.image.url}/large",
|
||||
"public": f"{obj.banner_image.image.url}/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.id,
|
||||
"image_url": latest_photo.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{latest_photo.image.url}/thumbnail",
|
||||
"medium": f"{latest_photo.image.url}/medium",
|
||||
"large": f"{latest_photo.image.url}/large",
|
||||
"public": f"{latest_photo.image.url}/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.id,
|
||||
"image_url": obj.card_image.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{obj.card_image.image.url}/thumbnail",
|
||||
"medium": f"{obj.card_image.image.url}/medium",
|
||||
"large": f"{obj.card_image.image.url}/large",
|
||||
"public": f"{obj.card_image.image.url}/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.id,
|
||||
"image_url": latest_photo.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{latest_photo.image.url}/thumbnail",
|
||||
"medium": f"{latest_photo.image.url}/medium",
|
||||
"large": f"{latest_photo.image.url}/large",
|
||||
"public": f"{latest_photo.image.url}/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:
|
||||
photo = 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:
|
||||
photo = 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."""
|
||||
|
||||
|
||||
808
backend/apps/api/v1/serializers/ride_models.py
Normal file
808
backend/apps/api/v1/serializers/ride_models.py
Normal file
@@ -0,0 +1,808 @@
|
||||
"""
|
||||
RideModel serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains all serializers related to ride models, variants,
|
||||
technical specifications, and related functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
from .shared import ModelChoices
|
||||
|
||||
# Use dynamic imports to avoid circular import issues
|
||||
|
||||
|
||||
def get_ride_model_classes():
|
||||
"""Get ride model classes dynamically to avoid import issues."""
|
||||
from apps.rides.models import RideModel, RideModelVariant, RideModelPhoto, RideModelTechnicalSpec
|
||||
return RideModel, RideModelVariant, RideModelPhoto, RideModelTechnicalSpec
|
||||
|
||||
|
||||
# === RIDE MODEL SERIALIZERS ===
|
||||
|
||||
|
||||
class RideModelManufacturerOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model's manufacturer data."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
|
||||
|
||||
class RideModelPhotoOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model photos."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
image_url = serializers.SerializerMethodField()
|
||||
caption = serializers.CharField()
|
||||
alt_text = serializers.CharField()
|
||||
photo_type = serializers.CharField()
|
||||
is_primary = serializers.BooleanField()
|
||||
photographer = serializers.CharField()
|
||||
source = serializers.CharField()
|
||||
|
||||
@extend_schema_field(serializers.URLField(allow_null=True))
|
||||
def get_image_url(self, obj):
|
||||
"""Get the image URL."""
|
||||
if obj.image:
|
||||
return obj.image.url
|
||||
return None
|
||||
|
||||
|
||||
class RideModelTechnicalSpecOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model technical specifications."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
spec_category = serializers.CharField()
|
||||
spec_name = serializers.CharField()
|
||||
spec_value = serializers.CharField()
|
||||
spec_unit = serializers.CharField()
|
||||
notes = serializers.CharField()
|
||||
|
||||
|
||||
class RideModelVariantOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model variants."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
min_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, allow_null=True)
|
||||
max_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, allow_null=True)
|
||||
min_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, allow_null=True)
|
||||
max_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, allow_null=True)
|
||||
distinguishing_features = serializers.CharField()
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Ride Model List Example",
|
||||
summary="Example ride model list response",
|
||||
description="A typical ride model in the list view",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Hyper Coaster",
|
||||
"slug": "bolliger-mabillard-hyper-coaster",
|
||||
"category": "RC",
|
||||
"description": "High-speed steel roller coaster with airtime hills",
|
||||
"manufacturer": {
|
||||
"id": 1,
|
||||
"name": "Bolliger & Mabillard",
|
||||
"slug": "bolliger-mabillard"
|
||||
},
|
||||
"target_market": "THRILL",
|
||||
"is_discontinued": False,
|
||||
"total_installations": 15,
|
||||
"first_installation_year": 1999,
|
||||
"height_range_display": "200-325 ft",
|
||||
"speed_range_display": "70-95 mph",
|
||||
"primary_image": {
|
||||
"id": 123,
|
||||
"image_url": "https://example.com/image.jpg",
|
||||
"caption": "B&M Hyper Coaster",
|
||||
"photo_type": "PROMOTIONAL"
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class RideModelListOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model list view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
# Manufacturer info
|
||||
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
|
||||
|
||||
# Market info
|
||||
target_market = serializers.CharField()
|
||||
is_discontinued = serializers.BooleanField()
|
||||
total_installations = serializers.IntegerField()
|
||||
first_installation_year = serializers.IntegerField(allow_null=True)
|
||||
last_installation_year = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Display properties
|
||||
height_range_display = serializers.CharField()
|
||||
speed_range_display = serializers.CharField()
|
||||
installation_years_range = serializers.CharField()
|
||||
|
||||
# Primary image
|
||||
primary_image = RideModelPhotoOutputSerializer(allow_null=True)
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Ride Model Detail Example",
|
||||
summary="Example ride model detail response",
|
||||
description="A complete ride model detail response",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Hyper Coaster",
|
||||
"slug": "bolliger-mabillard-hyper-coaster",
|
||||
"category": "RC",
|
||||
"description": "High-speed steel roller coaster featuring airtime hills and smooth ride experience",
|
||||
"manufacturer": {
|
||||
"id": 1,
|
||||
"name": "Bolliger & Mabillard",
|
||||
"slug": "bolliger-mabillard"
|
||||
},
|
||||
"typical_height_range_min_ft": 200.0,
|
||||
"typical_height_range_max_ft": 325.0,
|
||||
"typical_speed_range_min_mph": 70.0,
|
||||
"typical_speed_range_max_mph": 95.0,
|
||||
"typical_capacity_range_min": 1200,
|
||||
"typical_capacity_range_max": 1800,
|
||||
"track_type": "Tubular Steel",
|
||||
"support_structure": "Steel",
|
||||
"train_configuration": "2-3 trains, 7-9 cars per train, 4 seats per car",
|
||||
"restraint_system": "Clamshell lap bar",
|
||||
"target_market": "THRILL",
|
||||
"is_discontinued": False,
|
||||
"total_installations": 15,
|
||||
"first_installation_year": 1999,
|
||||
"notable_features": "Airtime hills, smooth ride, high capacity",
|
||||
"photos": [
|
||||
{
|
||||
"id": 123,
|
||||
"image_url": "https://example.com/image.jpg",
|
||||
"caption": "B&M Hyper Coaster",
|
||||
"photo_type": "PROMOTIONAL",
|
||||
"is_primary": True
|
||||
}
|
||||
],
|
||||
"variants": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Mega Coaster",
|
||||
"description": "200-299 ft height variant",
|
||||
"min_height_ft": 200.0,
|
||||
"max_height_ft": 299.0
|
||||
}
|
||||
],
|
||||
"technical_specs": [
|
||||
{
|
||||
"id": 1,
|
||||
"spec_category": "DIMENSIONS",
|
||||
"spec_name": "Track Width",
|
||||
"spec_value": "1435",
|
||||
"spec_unit": "mm"
|
||||
}
|
||||
],
|
||||
"installations": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Nitro",
|
||||
"park_name": "Six Flags Great Adventure",
|
||||
"opening_date": "2001-04-07"
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class RideModelDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model detail view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
# Manufacturer info
|
||||
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
|
||||
|
||||
# Technical specifications
|
||||
typical_height_range_min_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, allow_null=True
|
||||
)
|
||||
typical_height_range_max_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, allow_null=True
|
||||
)
|
||||
typical_speed_range_min_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, allow_null=True
|
||||
)
|
||||
typical_speed_range_max_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, allow_null=True
|
||||
)
|
||||
typical_capacity_range_min = serializers.IntegerField(allow_null=True)
|
||||
typical_capacity_range_max = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Design characteristics
|
||||
track_type = serializers.CharField()
|
||||
support_structure = serializers.CharField()
|
||||
train_configuration = serializers.CharField()
|
||||
restraint_system = serializers.CharField()
|
||||
|
||||
# Market information
|
||||
first_installation_year = serializers.IntegerField(allow_null=True)
|
||||
last_installation_year = serializers.IntegerField(allow_null=True)
|
||||
is_discontinued = serializers.BooleanField()
|
||||
total_installations = serializers.IntegerField()
|
||||
|
||||
# Design features
|
||||
notable_features = serializers.CharField()
|
||||
target_market = serializers.CharField()
|
||||
|
||||
# Display properties
|
||||
height_range_display = serializers.CharField()
|
||||
speed_range_display = serializers.CharField()
|
||||
installation_years_range = serializers.CharField()
|
||||
|
||||
# SEO metadata
|
||||
meta_title = serializers.CharField()
|
||||
meta_description = serializers.CharField()
|
||||
|
||||
# Related data
|
||||
photos = RideModelPhotoOutputSerializer(many=True)
|
||||
variants = RideModelVariantOutputSerializer(many=True)
|
||||
technical_specs = RideModelTechnicalSpecOutputSerializer(many=True)
|
||||
installations = serializers.SerializerMethodField()
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
|
||||
def get_installations(self, obj):
|
||||
"""Get ride installations using this model."""
|
||||
from django.apps import apps
|
||||
Ride = apps.get_model('rides', 'Ride')
|
||||
|
||||
installations = Ride.objects.filter(ride_model=obj).select_related('park')[:10]
|
||||
return [
|
||||
{
|
||||
"id": ride.id,
|
||||
"name": ride.name,
|
||||
"slug": ride.slug,
|
||||
"park_name": ride.park.name,
|
||||
"park_slug": ride.park.slug,
|
||||
"opening_date": ride.opening_date,
|
||||
"status": ride.status,
|
||||
}
|
||||
for ride in installations
|
||||
]
|
||||
|
||||
|
||||
class RideModelCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating ride models."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
category = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_ride_category_choices(),
|
||||
allow_blank=True,
|
||||
default=""
|
||||
)
|
||||
|
||||
# Required manufacturer
|
||||
manufacturer_id = serializers.IntegerField()
|
||||
|
||||
# Technical specifications
|
||||
typical_height_range_min_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_height_range_max_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_speed_range_min_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_speed_range_max_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_capacity_range_min = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
typical_capacity_range_max = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
|
||||
# Design characteristics
|
||||
track_type = serializers.CharField(max_length=100, allow_blank=True, default="")
|
||||
support_structure = serializers.CharField(
|
||||
max_length=100, allow_blank=True, default="")
|
||||
train_configuration = serializers.CharField(
|
||||
max_length=200, allow_blank=True, default="")
|
||||
restraint_system = serializers.CharField(
|
||||
max_length=100, allow_blank=True, default="")
|
||||
|
||||
# Market information
|
||||
first_installation_year = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1800, max_value=2100
|
||||
)
|
||||
last_installation_year = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1800, max_value=2100
|
||||
)
|
||||
is_discontinued = serializers.BooleanField(default=False)
|
||||
|
||||
# Design features
|
||||
notable_features = serializers.CharField(allow_blank=True, default="")
|
||||
target_market = serializers.ChoiceField(
|
||||
choices=[
|
||||
('FAMILY', 'Family'),
|
||||
('THRILL', 'Thrill'),
|
||||
('EXTREME', 'Extreme'),
|
||||
('KIDDIE', 'Kiddie'),
|
||||
('ALL_AGES', 'All Ages'),
|
||||
],
|
||||
allow_blank=True,
|
||||
default=""
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
# Height range validation
|
||||
min_height = attrs.get("typical_height_range_min_ft")
|
||||
max_height = attrs.get("typical_height_range_max_ft")
|
||||
|
||||
if min_height and max_height and min_height > max_height:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum height cannot be greater than maximum height"
|
||||
)
|
||||
|
||||
# Speed range validation
|
||||
min_speed = attrs.get("typical_speed_range_min_mph")
|
||||
max_speed = attrs.get("typical_speed_range_max_mph")
|
||||
|
||||
if min_speed and max_speed and min_speed > max_speed:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum speed cannot be greater than maximum speed"
|
||||
)
|
||||
|
||||
# Capacity range validation
|
||||
min_capacity = attrs.get("typical_capacity_range_min")
|
||||
max_capacity = attrs.get("typical_capacity_range_max")
|
||||
|
||||
if min_capacity and max_capacity and min_capacity > max_capacity:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum capacity cannot be greater than maximum capacity"
|
||||
)
|
||||
|
||||
# Installation years validation
|
||||
first_year = attrs.get("first_installation_year")
|
||||
last_year = attrs.get("last_installation_year")
|
||||
|
||||
if first_year and last_year and first_year > last_year:
|
||||
raise serializers.ValidationError(
|
||||
"First installation year cannot be after last installation year"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class RideModelUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride models."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
category = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_ride_category_choices(),
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
# Manufacturer
|
||||
manufacturer_id = serializers.IntegerField(required=False)
|
||||
|
||||
# Technical specifications
|
||||
typical_height_range_min_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_height_range_max_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_speed_range_min_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_speed_range_max_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
typical_capacity_range_min = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
typical_capacity_range_max = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
|
||||
# Design characteristics
|
||||
track_type = serializers.CharField(max_length=100, allow_blank=True, required=False)
|
||||
support_structure = serializers.CharField(
|
||||
max_length=100, allow_blank=True, required=False)
|
||||
train_configuration = serializers.CharField(
|
||||
max_length=200, allow_blank=True, required=False)
|
||||
restraint_system = serializers.CharField(
|
||||
max_length=100, allow_blank=True, required=False)
|
||||
|
||||
# Market information
|
||||
first_installation_year = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1800, max_value=2100
|
||||
)
|
||||
last_installation_year = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1800, max_value=2100
|
||||
)
|
||||
is_discontinued = serializers.BooleanField(required=False)
|
||||
|
||||
# Design features
|
||||
notable_features = serializers.CharField(allow_blank=True, required=False)
|
||||
target_market = serializers.ChoiceField(
|
||||
choices=[
|
||||
('FAMILY', 'Family'),
|
||||
('THRILL', 'Thrill'),
|
||||
('EXTREME', 'Extreme'),
|
||||
('KIDDIE', 'Kiddie'),
|
||||
('ALL_AGES', 'All Ages'),
|
||||
],
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
# Height range validation
|
||||
min_height = attrs.get("typical_height_range_min_ft")
|
||||
max_height = attrs.get("typical_height_range_max_ft")
|
||||
|
||||
if min_height and max_height and min_height > max_height:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum height cannot be greater than maximum height"
|
||||
)
|
||||
|
||||
# Speed range validation
|
||||
min_speed = attrs.get("typical_speed_range_min_mph")
|
||||
max_speed = attrs.get("typical_speed_range_max_mph")
|
||||
|
||||
if min_speed and max_speed and min_speed > max_speed:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum speed cannot be greater than maximum speed"
|
||||
)
|
||||
|
||||
# Capacity range validation
|
||||
min_capacity = attrs.get("typical_capacity_range_min")
|
||||
max_capacity = attrs.get("typical_capacity_range_max")
|
||||
|
||||
if min_capacity and max_capacity and min_capacity > max_capacity:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum capacity cannot be greater than maximum capacity"
|
||||
)
|
||||
|
||||
# Installation years validation
|
||||
first_year = attrs.get("first_installation_year")
|
||||
last_year = attrs.get("last_installation_year")
|
||||
|
||||
if first_year and last_year and first_year > last_year:
|
||||
raise serializers.ValidationError(
|
||||
"First installation year cannot be after last installation year"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class RideModelFilterInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for ride model filtering and search."""
|
||||
|
||||
# Search
|
||||
search = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Category filter
|
||||
category = serializers.MultipleChoiceField(
|
||||
choices=ModelChoices.get_ride_category_choices(),
|
||||
required=False
|
||||
)
|
||||
|
||||
# Manufacturer filter
|
||||
manufacturer_id = serializers.IntegerField(required=False)
|
||||
manufacturer_slug = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Market filter
|
||||
target_market = serializers.MultipleChoiceField(
|
||||
choices=[
|
||||
('FAMILY', 'Family'),
|
||||
('THRILL', 'Thrill'),
|
||||
('EXTREME', 'Extreme'),
|
||||
('KIDDIE', 'Kiddie'),
|
||||
('ALL_AGES', 'All Ages'),
|
||||
],
|
||||
required=False
|
||||
)
|
||||
|
||||
# Status filter
|
||||
is_discontinued = serializers.BooleanField(required=False)
|
||||
|
||||
# Year filters
|
||||
first_installation_year_min = serializers.IntegerField(required=False)
|
||||
first_installation_year_max = serializers.IntegerField(required=False)
|
||||
|
||||
# Installation count filter
|
||||
min_installations = serializers.IntegerField(required=False, min_value=0)
|
||||
|
||||
# Height filters
|
||||
min_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False
|
||||
)
|
||||
max_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False
|
||||
)
|
||||
|
||||
# Speed filters
|
||||
min_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False
|
||||
)
|
||||
max_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False
|
||||
)
|
||||
|
||||
# Ordering
|
||||
ordering = serializers.ChoiceField(
|
||||
choices=[
|
||||
"name",
|
||||
"-name",
|
||||
"manufacturer__name",
|
||||
"-manufacturer__name",
|
||||
"first_installation_year",
|
||||
"-first_installation_year",
|
||||
"total_installations",
|
||||
"-total_installations",
|
||||
"created_at",
|
||||
"-created_at",
|
||||
],
|
||||
required=False,
|
||||
default="manufacturer__name,name",
|
||||
)
|
||||
|
||||
|
||||
# === RIDE MODEL VARIANT SERIALIZERS ===
|
||||
|
||||
|
||||
class RideModelVariantCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating ride model variants."""
|
||||
|
||||
ride_model_id = serializers.IntegerField()
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
|
||||
# Variant-specific specifications
|
||||
min_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
max_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
min_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
max_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
|
||||
# Distinguishing features
|
||||
distinguishing_features = serializers.CharField(allow_blank=True, default="")
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
# Height range validation
|
||||
min_height = attrs.get("min_height_ft")
|
||||
max_height = attrs.get("max_height_ft")
|
||||
|
||||
if min_height and max_height and min_height > max_height:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum height cannot be greater than maximum height"
|
||||
)
|
||||
|
||||
# Speed range validation
|
||||
min_speed = attrs.get("min_speed_mph")
|
||||
max_speed = attrs.get("max_speed_mph")
|
||||
|
||||
if min_speed and max_speed and min_speed > max_speed:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum speed cannot be greater than maximum speed"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class RideModelVariantUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride model variants."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
|
||||
# Variant-specific specifications
|
||||
min_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
max_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
min_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
max_speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
|
||||
# Distinguishing features
|
||||
distinguishing_features = serializers.CharField(allow_blank=True, required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
# Height range validation
|
||||
min_height = attrs.get("min_height_ft")
|
||||
max_height = attrs.get("max_height_ft")
|
||||
|
||||
if min_height and max_height and min_height > max_height:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum height cannot be greater than maximum height"
|
||||
)
|
||||
|
||||
# Speed range validation
|
||||
min_speed = attrs.get("min_speed_mph")
|
||||
max_speed = attrs.get("max_speed_mph")
|
||||
|
||||
if min_speed and max_speed and min_speed > max_speed:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum speed cannot be greater than maximum speed"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
# === RIDE MODEL TECHNICAL SPEC SERIALIZERS ===
|
||||
|
||||
|
||||
class RideModelTechnicalSpecCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating ride model technical specifications."""
|
||||
|
||||
ride_model_id = serializers.IntegerField()
|
||||
spec_category = serializers.ChoiceField(
|
||||
choices=[
|
||||
('DIMENSIONS', 'Dimensions'),
|
||||
('PERFORMANCE', 'Performance'),
|
||||
('CAPACITY', 'Capacity'),
|
||||
('SAFETY', 'Safety Features'),
|
||||
('ELECTRICAL', 'Electrical Requirements'),
|
||||
('FOUNDATION', 'Foundation Requirements'),
|
||||
('MAINTENANCE', 'Maintenance'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
)
|
||||
spec_name = serializers.CharField(max_length=100)
|
||||
spec_value = serializers.CharField(max_length=255)
|
||||
spec_unit = serializers.CharField(max_length=20, allow_blank=True, default="")
|
||||
notes = serializers.CharField(allow_blank=True, default="")
|
||||
|
||||
|
||||
class RideModelTechnicalSpecUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride model technical specifications."""
|
||||
|
||||
spec_category = serializers.ChoiceField(
|
||||
choices=[
|
||||
('DIMENSIONS', 'Dimensions'),
|
||||
('PERFORMANCE', 'Performance'),
|
||||
('CAPACITY', 'Capacity'),
|
||||
('SAFETY', 'Safety Features'),
|
||||
('ELECTRICAL', 'Electrical Requirements'),
|
||||
('FOUNDATION', 'Foundation Requirements'),
|
||||
('MAINTENANCE', 'Maintenance'),
|
||||
('OTHER', 'Other'),
|
||||
],
|
||||
required=False
|
||||
)
|
||||
spec_name = serializers.CharField(max_length=100, required=False)
|
||||
spec_value = serializers.CharField(max_length=255, required=False)
|
||||
spec_unit = serializers.CharField(max_length=20, allow_blank=True, required=False)
|
||||
notes = serializers.CharField(allow_blank=True, required=False)
|
||||
|
||||
|
||||
# === RIDE MODEL PHOTO SERIALIZERS ===
|
||||
|
||||
|
||||
class RideModelPhotoCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating ride model photos."""
|
||||
|
||||
ride_model_id = serializers.IntegerField()
|
||||
image = serializers.ImageField()
|
||||
caption = serializers.CharField(max_length=500, allow_blank=True, default="")
|
||||
alt_text = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
photo_type = serializers.ChoiceField(
|
||||
choices=[
|
||||
('PROMOTIONAL', 'Promotional'),
|
||||
('TECHNICAL', 'Technical Drawing'),
|
||||
('INSTALLATION', 'Installation Example'),
|
||||
('RENDERING', '3D Rendering'),
|
||||
('CATALOG', 'Catalog Image'),
|
||||
],
|
||||
default='PROMOTIONAL'
|
||||
)
|
||||
is_primary = serializers.BooleanField(default=False)
|
||||
photographer = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
source = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
copyright_info = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
|
||||
|
||||
class RideModelPhotoUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride model photos."""
|
||||
|
||||
caption = serializers.CharField(max_length=500, allow_blank=True, required=False)
|
||||
alt_text = serializers.CharField(max_length=255, allow_blank=True, required=False)
|
||||
photo_type = serializers.ChoiceField(
|
||||
choices=[
|
||||
('PROMOTIONAL', 'Promotional'),
|
||||
('TECHNICAL', 'Technical Drawing'),
|
||||
('INSTALLATION', 'Installation Example'),
|
||||
('RENDERING', '3D Rendering'),
|
||||
('CATALOG', 'Catalog Image'),
|
||||
],
|
||||
required=False
|
||||
)
|
||||
is_primary = serializers.BooleanField(required=False)
|
||||
photographer = serializers.CharField(
|
||||
max_length=255, allow_blank=True, required=False)
|
||||
source = serializers.CharField(max_length=255, allow_blank=True, required=False)
|
||||
copyright_info = serializers.CharField(
|
||||
max_length=255, allow_blank=True, required=False)
|
||||
|
||||
|
||||
# === RIDE MODEL STATS SERIALIZERS ===
|
||||
|
||||
|
||||
class RideModelStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model statistics."""
|
||||
|
||||
total_models = serializers.IntegerField()
|
||||
total_installations = serializers.IntegerField()
|
||||
active_manufacturers = serializers.IntegerField()
|
||||
discontinued_models = serializers.IntegerField()
|
||||
by_category = serializers.DictField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="Model counts by category"
|
||||
)
|
||||
by_target_market = serializers.DictField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="Model counts by target market"
|
||||
)
|
||||
by_manufacturer = serializers.DictField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="Model counts by manufacturer"
|
||||
)
|
||||
recent_models = serializers.IntegerField(
|
||||
help_text="Models created in the last 30 days"
|
||||
)
|
||||
@@ -119,6 +119,33 @@ class RideListOutputSerializer(serializers.Serializer):
|
||||
"name": "Rocky Mountain Construction",
|
||||
"slug": "rocky-mountain-construction",
|
||||
},
|
||||
"photos": [
|
||||
{
|
||||
"id": 123,
|
||||
"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",
|
||||
"is_primary": True,
|
||||
"photo_type": "exterior"
|
||||
}
|
||||
],
|
||||
"primary_photo": {
|
||||
"id": 123,
|
||||
"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",
|
||||
"photo_type": "exterior"
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -161,6 +188,12 @@ class RideDetailOutputSerializer(serializers.Serializer):
|
||||
# Model
|
||||
ride_model = RideModelOutputSerializer(allow_null=True)
|
||||
|
||||
# Photos
|
||||
photos = serializers.SerializerMethodField()
|
||||
primary_photo = serializers.SerializerMethodField()
|
||||
banner_image = serializers.SerializerMethodField()
|
||||
card_image = serializers.SerializerMethodField()
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
@@ -195,6 +228,192 @@ class RideDetailOutputSerializer(serializers.Serializer):
|
||||
}
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
|
||||
def get_photos(self, obj):
|
||||
"""Get all approved photos for this ride."""
|
||||
from apps.rides.models import RidePhoto
|
||||
|
||||
photos = RidePhoto.objects.filter(
|
||||
ride=obj,
|
||||
is_approved=True
|
||||
).order_by('-is_primary', '-created_at')[:10] # Limit to 10 photos
|
||||
|
||||
return [
|
||||
{
|
||||
"id": photo.id,
|
||||
"image_url": photo.image.url if photo.image else None,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{photo.image.url}/thumbnail" if photo.image else None,
|
||||
"medium": f"{photo.image.url}/medium" if photo.image else None,
|
||||
"large": f"{photo.image.url}/large" if photo.image else None,
|
||||
"public": f"{photo.image.url}/public" if photo.image else None,
|
||||
} if photo.image else {},
|
||||
"caption": photo.caption,
|
||||
"alt_text": photo.alt_text,
|
||||
"is_primary": photo.is_primary,
|
||||
"photo_type": photo.photo_type,
|
||||
}
|
||||
for photo in photos
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||||
def get_primary_photo(self, obj):
|
||||
"""Get the primary photo for this ride."""
|
||||
from apps.rides.models import RidePhoto
|
||||
|
||||
try:
|
||||
photo = RidePhoto.objects.filter(
|
||||
ride=obj,
|
||||
is_primary=True,
|
||||
is_approved=True
|
||||
).first()
|
||||
|
||||
if photo and photo.image:
|
||||
return {
|
||||
"id": photo.id,
|
||||
"image_url": photo.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{photo.image.url}/thumbnail",
|
||||
"medium": f"{photo.image.url}/medium",
|
||||
"large": f"{photo.image.url}/large",
|
||||
"public": f"{photo.image.url}/public",
|
||||
},
|
||||
"caption": photo.caption,
|
||||
"alt_text": photo.alt_text,
|
||||
"photo_type": photo.photo_type,
|
||||
}
|
||||
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 ride 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.id,
|
||||
"image_url": obj.banner_image.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{obj.banner_image.image.url}/thumbnail",
|
||||
"medium": f"{obj.banner_image.image.url}/medium",
|
||||
"large": f"{obj.banner_image.image.url}/large",
|
||||
"public": f"{obj.banner_image.image.url}/public",
|
||||
},
|
||||
"caption": obj.banner_image.caption,
|
||||
"alt_text": obj.banner_image.alt_text,
|
||||
"photo_type": obj.banner_image.photo_type,
|
||||
}
|
||||
|
||||
# Fallback to latest approved photo
|
||||
from apps.rides.models import RidePhoto
|
||||
try:
|
||||
latest_photo = RidePhoto.objects.filter(
|
||||
ride=obj,
|
||||
is_approved=True,
|
||||
image__isnull=False
|
||||
).order_by('-created_at').first()
|
||||
|
||||
if latest_photo and latest_photo.image:
|
||||
return {
|
||||
"id": latest_photo.id,
|
||||
"image_url": latest_photo.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{latest_photo.image.url}/thumbnail",
|
||||
"medium": f"{latest_photo.image.url}/medium",
|
||||
"large": f"{latest_photo.image.url}/large",
|
||||
"public": f"{latest_photo.image.url}/public",
|
||||
},
|
||||
"caption": latest_photo.caption,
|
||||
"alt_text": latest_photo.alt_text,
|
||||
"photo_type": latest_photo.photo_type,
|
||||
"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 ride 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.id,
|
||||
"image_url": obj.card_image.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{obj.card_image.image.url}/thumbnail",
|
||||
"medium": f"{obj.card_image.image.url}/medium",
|
||||
"large": f"{obj.card_image.image.url}/large",
|
||||
"public": f"{obj.card_image.image.url}/public",
|
||||
},
|
||||
"caption": obj.card_image.caption,
|
||||
"alt_text": obj.card_image.alt_text,
|
||||
"photo_type": obj.card_image.photo_type,
|
||||
}
|
||||
|
||||
# Fallback to latest approved photo
|
||||
from apps.rides.models import RidePhoto
|
||||
try:
|
||||
latest_photo = RidePhoto.objects.filter(
|
||||
ride=obj,
|
||||
is_approved=True,
|
||||
image__isnull=False
|
||||
).order_by('-created_at').first()
|
||||
|
||||
if latest_photo and latest_photo.image:
|
||||
return {
|
||||
"id": latest_photo.id,
|
||||
"image_url": latest_photo.image.url,
|
||||
"image_variants": {
|
||||
"thumbnail": f"{latest_photo.image.url}/thumbnail",
|
||||
"medium": f"{latest_photo.image.url}/medium",
|
||||
"large": f"{latest_photo.image.url}/large",
|
||||
"public": f"{latest_photo.image.url}/public",
|
||||
},
|
||||
"caption": latest_photo.caption,
|
||||
"alt_text": latest_photo.alt_text,
|
||||
"photo_type": latest_photo.photo_type,
|
||||
"is_fallback": True,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class RideImageSettingsInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for setting ride 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 ride."""
|
||||
if value is not None:
|
||||
from apps.rides.models import RidePhoto
|
||||
try:
|
||||
photo = RidePhoto.objects.get(id=value)
|
||||
# The ride will be validated in the view
|
||||
return value
|
||||
except RidePhoto.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 ride."""
|
||||
if value is not None:
|
||||
from apps.rides.models import RidePhoto
|
||||
try:
|
||||
photo = RidePhoto.objects.get(id=value)
|
||||
# The ride will be validated in the view
|
||||
return value
|
||||
except RidePhoto.DoesNotExist:
|
||||
raise serializers.ValidationError("Photo not found")
|
||||
return value
|
||||
|
||||
|
||||
class RideCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating rides."""
|
||||
|
||||
@@ -102,6 +102,22 @@ class ModelChoices:
|
||||
("SBNO", "Standing But Not Operating"),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_ride_category_choices():
|
||||
try:
|
||||
from apps.rides.models import CATEGORY_CHOICES
|
||||
|
||||
return CATEGORY_CHOICES
|
||||
except ImportError:
|
||||
return [
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
]
|
||||
|
||||
|
||||
class LocationOutputSerializer(serializers.Serializer):
|
||||
"""Shared serializer for location data."""
|
||||
|
||||
Reference in New Issue
Block a user