""" 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 config.django import base as settings from .shared import ModelChoices from apps.core.choices.serializers import RichChoiceFieldSerializer # 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 = RichChoiceFieldSerializer( choice_group="categories", domain="rides" ) description = serializers.CharField() # Manufacturer info manufacturer = RideModelManufacturerOutputSerializer(allow_null=True) # Market info target_market = RichChoiceFieldSerializer( choice_group="target_markets", domain="rides" ) 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) # URL url = serializers.SerializerMethodField() # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() @extend_schema_field(serializers.URLField()) def get_url(self, obj) -> str: """Generate the frontend URL for this ride model.""" return f"{settings.FRONTEND_DOMAIN}/rides/manufacturers/{obj.manufacturer.slug}/{obj.slug}/" @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() # URL url = serializers.SerializerMethodField() # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() @extend_schema_field(serializers.URLField()) def get_url(self, obj) -> str: """Generate the frontend URL for this ride model.""" return f"{settings.FRONTEND_DOMAIN}/rides/manufacturers/{obj.manufacturer.slug}/{obj.slug}/" @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=ModelChoices.get_target_market_choices(), required=False, allow_blank=True, ) 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=ModelChoices.get_target_market_choices(), 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=ModelChoices.get_target_market_choices(), 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=ModelChoices.get_technical_spec_category_choices() ) 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=ModelChoices.get_technical_spec_category_choices(), 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=ModelChoices.get_photo_type_choices(), 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=ModelChoices.get_photo_type_choices(), 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" )