""" Rides domain serializers for ThrillWiki API v1. This module contains all serializers related to rides, roller coaster statistics, ride locations, and ride reviews. """ from rest_framework import serializers from drf_spectacular.utils import ( extend_schema_serializer, extend_schema_field, OpenApiExample, ) from .shared import ModelChoices # === RIDE SERIALIZERS === class RideParkOutputSerializer(serializers.Serializer): """Output serializer for ride's park data.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() class RideModelOutputSerializer(serializers.Serializer): """Output serializer for ride model data.""" id = serializers.IntegerField() name = serializers.CharField() description = serializers.CharField() category = serializers.CharField() manufacturer = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField(allow_null=True)) def get_manufacturer(self, obj) -> dict | None: if obj.manufacturer: return { "id": obj.manufacturer.id, "name": obj.manufacturer.name, "slug": obj.manufacturer.slug, } return None @extend_schema_serializer( examples=[ OpenApiExample( "Ride List Example", summary="Example ride list response", description="A typical ride in the list view", value={ "id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance", "category": "ROLLER_COASTER", "status": "OPERATING", "description": "Hybrid roller coaster", "park": {"id": 1, "name": "Cedar Point", "slug": "cedar-point"}, "average_rating": 4.8, "capacity_per_hour": 1200, "opening_date": "2018-05-05", }, ) ] ) class RideListOutputSerializer(serializers.Serializer): """Output serializer for ride list view.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() category = serializers.CharField() status = serializers.CharField() description = serializers.CharField() # Park info park = RideParkOutputSerializer() # Statistics average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) capacity_per_hour = serializers.IntegerField(allow_null=True) # Dates opening_date = serializers.DateField(allow_null=True) closing_date = serializers.DateField(allow_null=True) # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() @extend_schema_serializer( examples=[ OpenApiExample( "Ride Detail Example", summary="Example ride detail response", description="A complete ride detail response", value={ "id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance", "category": "ROLLER_COASTER", "status": "OPERATING", "description": "Hybrid roller coaster featuring RMC I-Box track", "park": {"id": 1, "name": "Cedar Point", "slug": "cedar-point"}, "opening_date": "2018-05-05", "min_height_in": 48, "capacity_per_hour": 1200, "ride_duration_seconds": 150, "average_rating": 4.8, "manufacturer": { "id": 1, "name": "Rocky Mountain Construction", "slug": "rocky-mountain-construction", }, }, ) ] ) class RideDetailOutputSerializer(serializers.Serializer): """Output serializer for ride detail view.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() category = serializers.CharField() status = serializers.CharField() post_closing_status = serializers.CharField(allow_null=True) description = serializers.CharField() # Park info park = RideParkOutputSerializer() park_area = serializers.SerializerMethodField() # Dates opening_date = serializers.DateField(allow_null=True) closing_date = serializers.DateField(allow_null=True) status_since = serializers.DateField(allow_null=True) # Physical specs min_height_in = serializers.IntegerField(allow_null=True) max_height_in = serializers.IntegerField(allow_null=True) capacity_per_hour = serializers.IntegerField(allow_null=True) ride_duration_seconds = serializers.IntegerField(allow_null=True) # Statistics average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) # Companies manufacturer = serializers.SerializerMethodField() designer = serializers.SerializerMethodField() # Model ride_model = RideModelOutputSerializer(allow_null=True) # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() @extend_schema_field(serializers.DictField(allow_null=True)) def get_park_area(self, obj) -> dict | None: if obj.park_area: return { "id": obj.park_area.id, "name": obj.park_area.name, "slug": obj.park_area.slug, } return None @extend_schema_field(serializers.DictField(allow_null=True)) def get_manufacturer(self, obj) -> dict | None: if obj.manufacturer: return { "id": obj.manufacturer.id, "name": obj.manufacturer.name, "slug": obj.manufacturer.slug, } return None @extend_schema_field(serializers.DictField(allow_null=True)) def get_designer(self, obj) -> dict | None: if obj.designer: return { "id": obj.designer.id, "name": obj.designer.name, "slug": obj.designer.slug, } return None class RideCreateInputSerializer(serializers.Serializer): """Input serializer for creating rides.""" name = serializers.CharField(max_length=255) description = serializers.CharField(allow_blank=True, default="") category = serializers.ChoiceField(choices=[]) # Choices set dynamically status = serializers.ChoiceField( choices=[], default="OPERATING" ) # Choices set dynamically # Required park park_id = serializers.IntegerField() # Optional area park_area_id = serializers.IntegerField(required=False, allow_null=True) # Optional dates opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) status_since = serializers.DateField(required=False, allow_null=True) # Optional specs min_height_in = serializers.IntegerField( required=False, allow_null=True, min_value=30, max_value=90 ) max_height_in = serializers.IntegerField( required=False, allow_null=True, min_value=30, max_value=90 ) capacity_per_hour = serializers.IntegerField( required=False, allow_null=True, min_value=1 ) ride_duration_seconds = serializers.IntegerField( required=False, allow_null=True, min_value=1 ) # Optional companies manufacturer_id = serializers.IntegerField(required=False, allow_null=True) designer_id = serializers.IntegerField(required=False, allow_null=True) # Optional model ride_model_id = serializers.IntegerField(required=False, allow_null=True) def validate(self, attrs): """Cross-field validation.""" # Date validation opening_date = attrs.get("opening_date") closing_date = attrs.get("closing_date") if opening_date and closing_date and closing_date < opening_date: raise serializers.ValidationError( "Closing date cannot be before opening date" ) # Height validation min_height = attrs.get("min_height_in") max_height = attrs.get("max_height_in") if min_height and max_height and min_height > max_height: raise serializers.ValidationError( "Minimum height cannot be greater than maximum height" ) return attrs class RideUpdateInputSerializer(serializers.Serializer): """Input serializer for updating rides.""" name = serializers.CharField(max_length=255, required=False) description = serializers.CharField(allow_blank=True, required=False) category = serializers.ChoiceField( choices=[], required=False ) # Choices set dynamically status = serializers.ChoiceField( choices=[], required=False ) # Choices set dynamically post_closing_status = serializers.ChoiceField( choices=ModelChoices.get_ride_post_closing_choices(), required=False, allow_null=True, ) # Park and area park_id = serializers.IntegerField(required=False) park_area_id = serializers.IntegerField(required=False, allow_null=True) # Dates opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) status_since = serializers.DateField(required=False, allow_null=True) # Specs min_height_in = serializers.IntegerField( required=False, allow_null=True, min_value=30, max_value=90 ) max_height_in = serializers.IntegerField( required=False, allow_null=True, min_value=30, max_value=90 ) capacity_per_hour = serializers.IntegerField( required=False, allow_null=True, min_value=1 ) ride_duration_seconds = serializers.IntegerField( required=False, allow_null=True, min_value=1 ) # Companies manufacturer_id = serializers.IntegerField(required=False, allow_null=True) designer_id = serializers.IntegerField(required=False, allow_null=True) # Model ride_model_id = serializers.IntegerField(required=False, allow_null=True) def validate(self, attrs): """Cross-field validation.""" # Date validation opening_date = attrs.get("opening_date") closing_date = attrs.get("closing_date") if opening_date and closing_date and closing_date < opening_date: raise serializers.ValidationError( "Closing date cannot be before opening date" ) # Height validation min_height = attrs.get("min_height_in") max_height = attrs.get("max_height_in") if min_height and max_height and min_height > max_height: raise serializers.ValidationError( "Minimum height cannot be greater than maximum height" ) return attrs class RideFilterInputSerializer(serializers.Serializer): """Input serializer for ride filtering and search.""" # Search search = serializers.CharField(required=False, allow_blank=True) # Category filter category = serializers.MultipleChoiceField( choices=[], required=False ) # Choices set dynamically # Status filter status = serializers.MultipleChoiceField( choices=[], required=False # Choices set dynamically ) # Park filter park_id = serializers.IntegerField(required=False) park_slug = serializers.CharField(required=False, allow_blank=True) # Company filters manufacturer_id = serializers.IntegerField(required=False) designer_id = serializers.IntegerField(required=False) # Rating filter min_rating = serializers.DecimalField( max_digits=3, decimal_places=2, required=False, min_value=1, max_value=10, ) # Height filters min_height_requirement = serializers.IntegerField(required=False) max_height_requirement = serializers.IntegerField(required=False) # Capacity filter min_capacity = serializers.IntegerField(required=False) # Ordering ordering = serializers.ChoiceField( choices=[ "name", "-name", "opening_date", "-opening_date", "average_rating", "-average_rating", "capacity_per_hour", "-capacity_per_hour", "created_at", "-created_at", ], required=False, default="name", ) # === ROLLER COASTER STATS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Roller Coaster Stats Example", summary="Example roller coaster statistics", description="Detailed statistics for a roller coaster", value={ "id": 1, "ride": {"id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance"}, "height_ft": 205.0, "length_ft": 5740.0, "speed_mph": 74.0, "inversions": 4, "ride_time_seconds": 150, "track_material": "HYBRID", "roller_coaster_type": "SITDOWN", "launch_type": "CHAIN", }, ) ] ) class RollerCoasterStatsOutputSerializer(serializers.Serializer): """Output serializer for roller coaster statistics.""" id = serializers.IntegerField() height_ft = serializers.DecimalField( max_digits=6, decimal_places=2, allow_null=True ) length_ft = serializers.DecimalField( max_digits=7, decimal_places=2, allow_null=True ) speed_mph = serializers.DecimalField( max_digits=5, decimal_places=2, allow_null=True ) inversions = serializers.IntegerField() ride_time_seconds = serializers.IntegerField(allow_null=True) track_type = serializers.CharField() track_material = serializers.CharField() roller_coaster_type = serializers.CharField() max_drop_height_ft = serializers.DecimalField( max_digits=6, decimal_places=2, allow_null=True ) launch_type = serializers.CharField() train_style = serializers.CharField() trains_count = serializers.IntegerField(allow_null=True) cars_per_train = serializers.IntegerField(allow_null=True) seats_per_car = serializers.IntegerField(allow_null=True) # Ride info ride = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_ride(self, obj) -> dict: return { "id": obj.ride.id, "name": obj.ride.name, "slug": obj.ride.slug, } class RollerCoasterStatsCreateInputSerializer(serializers.Serializer): """Input serializer for creating roller coaster statistics.""" ride_id = serializers.IntegerField() height_ft = serializers.DecimalField( max_digits=6, decimal_places=2, required=False, allow_null=True ) length_ft = serializers.DecimalField( max_digits=7, decimal_places=2, required=False, allow_null=True ) speed_mph = serializers.DecimalField( max_digits=5, decimal_places=2, required=False, allow_null=True ) inversions = serializers.IntegerField(default=0) ride_time_seconds = serializers.IntegerField(required=False, allow_null=True) track_type = serializers.CharField(max_length=255, allow_blank=True, default="") track_material = serializers.ChoiceField( choices=ModelChoices.get_coaster_track_choices(), default="STEEL" ) roller_coaster_type = serializers.ChoiceField( choices=ModelChoices.get_coaster_type_choices(), default="SITDOWN" ) max_drop_height_ft = serializers.DecimalField( max_digits=6, decimal_places=2, required=False, allow_null=True ) launch_type = serializers.ChoiceField( choices=ModelChoices.get_launch_choices(), default="CHAIN" ) train_style = serializers.CharField(max_length=255, allow_blank=True, default="") trains_count = serializers.IntegerField(required=False, allow_null=True) cars_per_train = serializers.IntegerField(required=False, allow_null=True) seats_per_car = serializers.IntegerField(required=False, allow_null=True) class RollerCoasterStatsUpdateInputSerializer(serializers.Serializer): """Input serializer for updating roller coaster statistics.""" height_ft = serializers.DecimalField( max_digits=6, decimal_places=2, required=False, allow_null=True ) length_ft = serializers.DecimalField( max_digits=7, decimal_places=2, required=False, allow_null=True ) speed_mph = serializers.DecimalField( max_digits=5, decimal_places=2, required=False, allow_null=True ) inversions = serializers.IntegerField(required=False) ride_time_seconds = serializers.IntegerField(required=False, allow_null=True) track_type = serializers.CharField(max_length=255, allow_blank=True, required=False) track_material = serializers.ChoiceField( choices=ModelChoices.get_coaster_track_choices(), required=False ) roller_coaster_type = serializers.ChoiceField( choices=ModelChoices.get_coaster_type_choices(), required=False ) max_drop_height_ft = serializers.DecimalField( max_digits=6, decimal_places=2, required=False, allow_null=True ) launch_type = serializers.ChoiceField( choices=ModelChoices.get_launch_choices(), required=False ) train_style = serializers.CharField( max_length=255, allow_blank=True, required=False ) trains_count = serializers.IntegerField(required=False, allow_null=True) cars_per_train = serializers.IntegerField(required=False, allow_null=True) seats_per_car = serializers.IntegerField(required=False, allow_null=True) # === RIDE LOCATION SERIALIZERS === class RideLocationOutputSerializer(serializers.Serializer): """Output serializer for ride locations.""" id = serializers.IntegerField() latitude = serializers.FloatField(allow_null=True) longitude = serializers.FloatField(allow_null=True) coordinates = serializers.CharField() # Ride info ride = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_ride(self, obj) -> dict: return { "id": obj.ride.id, "name": obj.ride.name, "slug": obj.ride.slug, } class RideLocationCreateInputSerializer(serializers.Serializer): """Input serializer for creating ride locations.""" ride_id = serializers.IntegerField() latitude = serializers.FloatField(required=False, allow_null=True) longitude = serializers.FloatField(required=False, allow_null=True) class RideLocationUpdateInputSerializer(serializers.Serializer): """Input serializer for updating ride locations.""" latitude = serializers.FloatField(required=False, allow_null=True) longitude = serializers.FloatField(required=False, allow_null=True) # === RIDE REVIEW SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Ride Review Example", summary="Example ride review response", description="A user review of a ride", value={ "id": 1, "rating": 9, "title": "Amazing coaster!", "content": "This ride was incredible, the airtime was fantastic.", "visit_date": "2024-08-15", "ride": {"id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance"}, "user": {"username": "coaster_fan", "display_name": "Coaster Fan"}, "created_at": "2024-08-16T10:30:00Z", "is_published": True, }, ) ] ) class RideReviewOutputSerializer(serializers.Serializer): """Output serializer for ride reviews.""" id = serializers.IntegerField() rating = serializers.IntegerField() title = serializers.CharField() content = serializers.CharField() visit_date = serializers.DateField() created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() is_published = serializers.BooleanField() # Ride info ride = serializers.SerializerMethodField() # User info (limited for privacy) user = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_ride(self, obj) -> dict: return { "id": obj.ride.id, "name": obj.ride.name, "slug": obj.ride.slug, } @extend_schema_field(serializers.DictField()) def get_user(self, obj) -> dict: return { "username": obj.user.username, "display_name": obj.user.get_display_name(), } class RideReviewCreateInputSerializer(serializers.Serializer): """Input serializer for creating ride reviews.""" ride_id = serializers.IntegerField() rating = serializers.IntegerField(min_value=1, max_value=10) title = serializers.CharField(max_length=200) content = serializers.CharField() visit_date = serializers.DateField() def validate_visit_date(self, value): """Validate visit date is not in the future.""" from django.utils import timezone if value > timezone.now().date(): raise serializers.ValidationError("Visit date cannot be in the future") return value class RideReviewUpdateInputSerializer(serializers.Serializer): """Input serializer for updating ride reviews.""" rating = serializers.IntegerField(min_value=1, max_value=10, required=False) title = serializers.CharField(max_length=200, required=False) content = serializers.CharField(required=False) visit_date = serializers.DateField(required=False) def validate_visit_date(self, value): """Validate visit date is not in the future.""" from django.utils import timezone if value and value > timezone.now().date(): raise serializers.ValidationError("Visit date cannot be in the future") return value