""" Consolidated serializers for ThrillWiki API v1. This module consolidates all API serializers from different apps into a unified structure following Django REST Framework and drf-spectacular best practices. """ from rest_framework import serializers from drf_spectacular.utils import ( extend_schema_serializer, extend_schema_field, OpenApiExample, ) from django.contrib.auth import get_user_model from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.crypto import get_random_string from django.utils import timezone from datetime import timedelta from django.contrib.sites.shortcuts import get_current_site from django.template.loader import render_to_string # Import models from different apps from apps.parks.models import Park from apps.rides.models import Ride from apps.rides.models.rides import CATEGORY_CHOICES from apps.accounts.models import User, PasswordReset from apps.email_service.services import EmailService # Import additional models that need API serializers from apps.parks.models import ParkArea, ParkLocation, ParkReview, Company from apps.rides.models import RideModel, RollerCoasterStats, RideLocation, RideReview from apps.accounts.models import UserProfile, TopList, TopListItem UserModel = get_user_model() # === SHARED/COMMON SERIALIZERS === class LocationOutputSerializer(serializers.Serializer): """Shared serializer for location data.""" latitude = serializers.SerializerMethodField() longitude = serializers.SerializerMethodField() city = serializers.SerializerMethodField() state = serializers.SerializerMethodField() country = serializers.SerializerMethodField() formatted_address = serializers.SerializerMethodField() @extend_schema_field(serializers.FloatField(allow_null=True)) def get_latitude(self, obj) -> float | None: if hasattr(obj, "location") and obj.location: return obj.location.latitude return None @extend_schema_field(serializers.FloatField(allow_null=True)) def get_longitude(self, obj) -> float | None: if hasattr(obj, "location") and obj.location: return obj.location.longitude return None @extend_schema_field(serializers.CharField(allow_null=True)) def get_city(self, obj) -> str | None: if hasattr(obj, "location") and obj.location: return obj.location.city return None @extend_schema_field(serializers.CharField(allow_null=True)) def get_state(self, obj) -> str | None: if hasattr(obj, "location") and obj.location: return obj.location.state return None @extend_schema_field(serializers.CharField(allow_null=True)) def get_country(self, obj) -> str | None: if hasattr(obj, "location") and obj.location: return obj.location.country return None @extend_schema_field(serializers.CharField()) def get_formatted_address(self, obj) -> str: if hasattr(obj, "location") and obj.location: return obj.location.formatted_address return "" class CompanyOutputSerializer(serializers.Serializer): """Shared serializer for company data.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() roles = serializers.ListField(child=serializers.CharField(), required=False) # === PARK SERIALIZERS === # ParkAreaOutputSerializer moved to comprehensive section below @extend_schema_serializer( examples=[ OpenApiExample( "Park List Example", summary="Example park list response", description="A typical park in the list view", value={ "id": 1, "name": "Cedar Point", "slug": "cedar-point", "status": "OPERATING", "description": "America's Roller Coast", "average_rating": 4.5, "coaster_count": 17, "ride_count": 70, "location": { "city": "Sandusky", "state": "Ohio", "country": "United States", }, "operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"}, }, ) ] ) class ParkListOutputSerializer(serializers.Serializer): """Output serializer for park list view.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() status = serializers.CharField() description = serializers.CharField() # Statistics average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) coaster_count = serializers.IntegerField(allow_null=True) ride_count = serializers.IntegerField(allow_null=True) # Location (simplified for list view) location = LocationOutputSerializer(allow_null=True) # Operator info operator = CompanyOutputSerializer() # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() @extend_schema_serializer( examples=[ OpenApiExample( "Park Detail Example", summary="Example park detail response", description="A complete park detail response", value={ "id": 1, "name": "Cedar Point", "slug": "cedar-point", "status": "OPERATING", "description": "America's Roller Coast", "opening_date": "1870-01-01", "website": "https://cedarpoint.com", "size_acres": 364.0, "average_rating": 4.5, "coaster_count": 17, "ride_count": 70, "location": { "latitude": 41.4793, "longitude": -82.6833, "city": "Sandusky", "state": "Ohio", "country": "United States", }, "operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"}, }, ) ] ) class ParkDetailOutputSerializer(serializers.Serializer): """Output serializer for park detail view.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() status = serializers.CharField() description = serializers.CharField() # Details opening_date = serializers.DateField(allow_null=True) closing_date = serializers.DateField(allow_null=True) operating_season = serializers.CharField() size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, allow_null=True ) website = serializers.URLField() # Statistics average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) coaster_count = serializers.IntegerField(allow_null=True) ride_count = serializers.IntegerField(allow_null=True) # Location (full details) location = LocationOutputSerializer(allow_null=True) # Companies operator = CompanyOutputSerializer() property_owner = CompanyOutputSerializer(allow_null=True) # Areas areas = serializers.SerializerMethodField() @extend_schema_field(serializers.ListField(child=serializers.DictField())) def get_areas(self, obj): """Get simplified area information.""" if hasattr(obj, "areas"): return [ { "id": area.id, "name": area.name, "slug": area.slug, "description": area.description, } for area in obj.areas.all() ] return [] # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() class ParkCreateInputSerializer(serializers.Serializer): """Input serializer for creating parks.""" name = serializers.CharField(max_length=255) description = serializers.CharField(allow_blank=True, default="") status = serializers.ChoiceField(choices=Park.STATUS_CHOICES, default="OPERATING") # Optional details opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) operating_season = serializers.CharField( max_length=255, required=False, allow_blank=True ) size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, allow_null=True ) website = serializers.URLField(required=False, allow_blank=True) # Required operator operator_id = serializers.IntegerField() # Optional property owner property_owner_id = serializers.IntegerField(required=False, allow_null=True) def validate(self, attrs): """Cross-field 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" ) return attrs class ParkUpdateInputSerializer(serializers.Serializer): """Input serializer for updating parks.""" name = serializers.CharField(max_length=255, required=False) description = serializers.CharField(allow_blank=True, required=False) status = serializers.ChoiceField(choices=Park.STATUS_CHOICES, required=False) # Optional details opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) operating_season = serializers.CharField( max_length=255, required=False, allow_blank=True ) size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, allow_null=True ) website = serializers.URLField(required=False, allow_blank=True) # Companies operator_id = serializers.IntegerField(required=False) property_owner_id = serializers.IntegerField(required=False, allow_null=True) def validate(self, attrs): """Cross-field 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" ) return attrs class ParkFilterInputSerializer(serializers.Serializer): """Input serializer for park filtering and search.""" # Search search = serializers.CharField(required=False, allow_blank=True) # Status filter status = serializers.MultipleChoiceField( choices=Park.STATUS_CHOICES, required=False ) # Location filters country = serializers.CharField(required=False, allow_blank=True) state = serializers.CharField(required=False, allow_blank=True) city = serializers.CharField(required=False, allow_blank=True) # Rating filter min_rating = serializers.DecimalField( max_digits=3, decimal_places=2, required=False, min_value=1, max_value=10, ) # Size filter min_size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, min_value=0 ) max_size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, required=False, min_value=0 ) # Company filters operator_id = serializers.IntegerField(required=False) property_owner_id = serializers.IntegerField(required=False) # Ordering ordering = serializers.ChoiceField( choices=[ "name", "-name", "opening_date", "-opening_date", "average_rating", "-average_rating", "coaster_count", "-coaster_count", "created_at", "-created_at", ], required=False, default="name", ) # === 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=CATEGORY_CHOICES) status = serializers.ChoiceField(choices=Ride.STATUS_CHOICES, default="OPERATING") # 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=CATEGORY_CHOICES, required=False) status = serializers.ChoiceField(choices=Ride.STATUS_CHOICES, required=False) post_closing_status = serializers.ChoiceField( choices=Ride.POST_CLOSING_STATUS_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=CATEGORY_CHOICES, required=False) # Status filter status = serializers.MultipleChoiceField( choices=Ride.STATUS_CHOICES, required=False ) # 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", ) # === PARK AREA SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Park Area Example", summary="Example park area response", description="A themed area within a park", value={ "id": 1, "name": "Tomorrowland", "slug": "tomorrowland", "description": "A futuristic themed area", "park": {"id": 1, "name": "Magic Kingdom", "slug": "magic-kingdom"}, "opening_date": "1971-10-01", "closing_date": None, }, ) ] ) class ParkAreaDetailOutputSerializer(serializers.Serializer): """Output serializer for park areas.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() description = serializers.CharField() opening_date = serializers.DateField(allow_null=True) closing_date = serializers.DateField(allow_null=True) # Park info park = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_park(self, obj) -> dict: return { "id": obj.park.id, "name": obj.park.name, "slug": obj.park.slug, } class ParkAreaCreateInputSerializer(serializers.Serializer): """Input serializer for creating park areas.""" name = serializers.CharField(max_length=255) description = serializers.CharField(allow_blank=True, default="") park_id = serializers.IntegerField() opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) def validate(self, attrs): """Cross-field 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" ) return attrs class ParkAreaUpdateInputSerializer(serializers.Serializer): """Input serializer for updating park areas.""" name = serializers.CharField(max_length=255, required=False) description = serializers.CharField(allow_blank=True, required=False) opening_date = serializers.DateField(required=False, allow_null=True) closing_date = serializers.DateField(required=False, allow_null=True) def validate(self, attrs): """Cross-field 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" ) return attrs # === PARK LOCATION SERIALIZERS === class ParkLocationOutputSerializer(serializers.Serializer): """Output serializer for park locations.""" id = serializers.IntegerField() latitude = serializers.FloatField(allow_null=True) longitude = serializers.FloatField(allow_null=True) address = serializers.CharField() city = serializers.CharField() state = serializers.CharField() country = serializers.CharField() postal_code = serializers.CharField() formatted_address = serializers.CharField() # Park info park = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_park(self, obj) -> dict: return { "id": obj.park.id, "name": obj.park.name, "slug": obj.park.slug, } class ParkLocationCreateInputSerializer(serializers.Serializer): """Input serializer for creating park locations.""" park_id = serializers.IntegerField() latitude = serializers.FloatField(required=False, allow_null=True) longitude = serializers.FloatField(required=False, allow_null=True) address = serializers.CharField(max_length=255, allow_blank=True, default="") city = serializers.CharField(max_length=100) state = serializers.CharField(max_length=100) country = serializers.CharField(max_length=100) postal_code = serializers.CharField(max_length=20, allow_blank=True, default="") class ParkLocationUpdateInputSerializer(serializers.Serializer): """Input serializer for updating park locations.""" latitude = serializers.FloatField(required=False, allow_null=True) longitude = serializers.FloatField(required=False, allow_null=True) address = serializers.CharField(max_length=255, allow_blank=True, required=False) city = serializers.CharField(max_length=100, required=False) state = serializers.CharField(max_length=100, required=False) country = serializers.CharField(max_length=100, required=False) postal_code = serializers.CharField(max_length=20, allow_blank=True, required=False) # === COMPANY SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Company Example", summary="Example company response", description="A company that operates parks or manufactures rides", value={ "id": 1, "name": "Cedar Fair", "slug": "cedar-fair", "roles": ["OPERATOR", "PROPERTY_OWNER"], "description": "Theme park operator based in Ohio", "website": "https://cedarfair.com", "founded_date": "1983-01-01", "rides_count": 0, "coasters_count": 0, }, ) ] ) class CompanyDetailOutputSerializer(serializers.Serializer): """Output serializer for company details.""" id = serializers.IntegerField() name = serializers.CharField() slug = serializers.CharField() roles = serializers.ListField(child=serializers.CharField()) description = serializers.CharField() website = serializers.URLField() founded_date = serializers.DateField(allow_null=True) rides_count = serializers.IntegerField() coasters_count = serializers.IntegerField() # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() class CompanyCreateInputSerializer(serializers.Serializer): """Input serializer for creating companies.""" name = serializers.CharField(max_length=255) roles = serializers.ListField( child=serializers.ChoiceField(choices=Company.CompanyRole.choices), allow_empty=False, ) description = serializers.CharField(allow_blank=True, default="") website = serializers.URLField(required=False, allow_blank=True) founded_date = serializers.DateField(required=False, allow_null=True) class CompanyUpdateInputSerializer(serializers.Serializer): """Input serializer for updating companies.""" name = serializers.CharField(max_length=255, required=False) roles = serializers.ListField( child=serializers.ChoiceField(choices=Company.CompanyRole.choices), required=False, ) description = serializers.CharField(allow_blank=True, required=False) website = serializers.URLField(required=False, allow_blank=True) founded_date = serializers.DateField(required=False, allow_null=True) # === RIDE MODEL SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Ride Model Example", summary="Example ride model response", description="A specific model/type of ride manufactured by a company", value={ "id": 1, "name": "Dive Coaster", "description": "A roller coaster featuring a near-vertical drop", "category": "RC", "manufacturer": { "id": 1, "name": "Bolliger & Mabillard", "slug": "bolliger-mabillard", }, }, ) ] ) class RideModelDetailOutputSerializer(serializers.Serializer): """Output serializer for ride model details.""" id = serializers.IntegerField() name = serializers.CharField() description = serializers.CharField() category = serializers.CharField() # Manufacturer info manufacturer = serializers.SerializerMethodField() # Metadata created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() @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 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=CATEGORY_CHOICES, required=False) manufacturer_id = serializers.IntegerField(required=False, allow_null=True) 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=CATEGORY_CHOICES, required=False) manufacturer_id = serializers.IntegerField(required=False, allow_null=True) # === 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=RollerCoasterStats.TRACK_MATERIAL_CHOICES, default="STEEL" ) roller_coaster_type = serializers.ChoiceField( choices=RollerCoasterStats.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=RollerCoasterStats.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=RollerCoasterStats.TRACK_MATERIAL_CHOICES, required=False ) roller_coaster_type = serializers.ChoiceField( choices=RollerCoasterStats.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=RollerCoasterStats.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 # === USER PROFILE SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Profile Example", summary="Example user profile response", description="A user's profile information", value={ "id": 1, "profile_id": "1234", "display_name": "Coaster Enthusiast", "bio": "Love visiting theme parks around the world!", "pronouns": "they/them", "avatar_url": "/media/avatars/user1.jpg", "coaster_credits": 150, "dark_ride_credits": 45, "flat_ride_credits": 80, "water_ride_credits": 25, "user": { "username": "coaster_fan", "date_joined": "2024-01-01T00:00:00Z", }, }, ) ] ) class UserProfileOutputSerializer(serializers.Serializer): """Output serializer for user profiles.""" id = serializers.IntegerField() profile_id = serializers.CharField() display_name = serializers.CharField() bio = serializers.CharField() pronouns = serializers.CharField() avatar_url = serializers.SerializerMethodField() twitter = serializers.URLField() instagram = serializers.URLField() youtube = serializers.URLField() discord = serializers.CharField() # Ride statistics coaster_credits = serializers.IntegerField() dark_ride_credits = serializers.IntegerField() flat_ride_credits = serializers.IntegerField() water_ride_credits = serializers.IntegerField() # User info (limited) user = serializers.SerializerMethodField() @extend_schema_field(serializers.URLField(allow_null=True)) def get_avatar_url(self, obj) -> str | None: return obj.get_avatar() @extend_schema_field(serializers.DictField()) def get_user(self, obj) -> dict: return { "username": obj.user.username, "date_joined": obj.user.date_joined, } class UserProfileCreateInputSerializer(serializers.Serializer): """Input serializer for creating user profiles.""" display_name = serializers.CharField(max_length=50) bio = serializers.CharField(max_length=500, allow_blank=True, default="") pronouns = serializers.CharField(max_length=50, allow_blank=True, default="") twitter = serializers.URLField(required=False, allow_blank=True) instagram = serializers.URLField(required=False, allow_blank=True) youtube = serializers.URLField(required=False, allow_blank=True) discord = serializers.CharField(max_length=100, allow_blank=True, default="") class UserProfileUpdateInputSerializer(serializers.Serializer): """Input serializer for updating user profiles.""" display_name = serializers.CharField(max_length=50, required=False) bio = serializers.CharField(max_length=500, allow_blank=True, required=False) pronouns = serializers.CharField(max_length=50, allow_blank=True, required=False) twitter = serializers.URLField(required=False, allow_blank=True) instagram = serializers.URLField(required=False, allow_blank=True) youtube = serializers.URLField(required=False, allow_blank=True) discord = serializers.CharField(max_length=100, allow_blank=True, required=False) coaster_credits = serializers.IntegerField(required=False) dark_ride_credits = serializers.IntegerField(required=False) flat_ride_credits = serializers.IntegerField(required=False) water_ride_credits = serializers.IntegerField(required=False) # === TOP LIST SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Top List Example", summary="Example top list response", description="A user's top list of rides or parks", value={ "id": 1, "title": "My Top 10 Roller Coasters", "category": "RC", "description": "My favorite roller coasters ranked", "user": {"username": "coaster_fan", "display_name": "Coaster Fan"}, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-08-15T12:00:00Z", }, ) ] ) class TopListOutputSerializer(serializers.Serializer): """Output serializer for top lists.""" id = serializers.IntegerField() title = serializers.CharField() category = serializers.CharField() description = serializers.CharField() created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() # User info user = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_user(self, obj) -> dict: return { "username": obj.user.username, "display_name": obj.user.get_display_name(), } class TopListCreateInputSerializer(serializers.Serializer): """Input serializer for creating top lists.""" title = serializers.CharField(max_length=100) category = serializers.ChoiceField(choices=TopList.Categories.choices) description = serializers.CharField(allow_blank=True, default="") class TopListUpdateInputSerializer(serializers.Serializer): """Input serializer for updating top lists.""" title = serializers.CharField(max_length=100, required=False) category = serializers.ChoiceField( choices=TopList.Categories.choices, required=False ) description = serializers.CharField(allow_blank=True, required=False) # === TOP LIST ITEM SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Top List Item Example", summary="Example top list item response", description="An item in a user's top list", value={ "id": 1, "rank": 1, "notes": "Amazing airtime and smooth ride", "object_name": "Steel Vengeance", "object_type": "Ride", "top_list": {"id": 1, "title": "My Top 10 Roller Coasters"}, }, ) ] ) class TopListItemOutputSerializer(serializers.Serializer): """Output serializer for top list items.""" id = serializers.IntegerField() rank = serializers.IntegerField() notes = serializers.CharField() object_name = serializers.SerializerMethodField() object_type = serializers.SerializerMethodField() # Top list info top_list = serializers.SerializerMethodField() @extend_schema_field(serializers.CharField()) def get_object_name(self, obj) -> str: """Get the name of the referenced object.""" # This would need to be implemented based on the generic foreign key return "Object Name" # Placeholder @extend_schema_field(serializers.CharField()) def get_object_type(self, obj) -> str: """Get the type of the referenced object.""" return obj.content_type.model_class().__name__ @extend_schema_field(serializers.DictField()) def get_top_list(self, obj) -> dict: return { "id": obj.top_list.id, "title": obj.top_list.title, } class TopListItemCreateInputSerializer(serializers.Serializer): """Input serializer for creating top list items.""" top_list_id = serializers.IntegerField() content_type_id = serializers.IntegerField() object_id = serializers.IntegerField() rank = serializers.IntegerField(min_value=1) notes = serializers.CharField(allow_blank=True, default="") class TopListItemUpdateInputSerializer(serializers.Serializer): """Input serializer for updating top list items.""" rank = serializers.IntegerField(min_value=1, required=False) notes = serializers.CharField(allow_blank=True, required=False) # === STATISTICS SERIALIZERS === class ParkStatsOutputSerializer(serializers.Serializer): """Output serializer for park statistics.""" total_parks = serializers.IntegerField() operating_parks = serializers.IntegerField() closed_parks = serializers.IntegerField() under_construction = serializers.IntegerField() # Averages average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) average_coaster_count = serializers.DecimalField( max_digits=5, decimal_places=2, allow_null=True ) # Top countries top_countries = serializers.ListField(child=serializers.DictField()) # Recently added recently_added_count = serializers.IntegerField() class RideStatsOutputSerializer(serializers.Serializer): """Output serializer for ride statistics.""" total_rides = serializers.IntegerField() operating_rides = serializers.IntegerField() closed_rides = serializers.IntegerField() under_construction = serializers.IntegerField() # By category rides_by_category = serializers.DictField() # Averages average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, allow_null=True ) average_capacity = serializers.DecimalField( max_digits=8, decimal_places=2, allow_null=True ) # Top manufacturers top_manufacturers = serializers.ListField(child=serializers.DictField()) # Recently added recently_added_count = serializers.IntegerField() # === REVIEW SERIALIZERS === class ParkReviewOutputSerializer(serializers.Serializer): """Output serializer for park reviews.""" id = serializers.IntegerField() rating = serializers.IntegerField() title = serializers.CharField() content = serializers.CharField() visit_date = serializers.DateField() created_at = serializers.DateTimeField() # User info (limited for privacy) user = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_user(self, obj) -> dict: return { "username": obj.user.username, "display_name": obj.user.get_full_name() or obj.user.username, } # === ACCOUNTS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Example", summary="Example user response", description="A typical user object", value={ "id": 1, "username": "john_doe", "email": "john@example.com", "first_name": "John", "last_name": "Doe", "date_joined": "2024-01-01T12:00:00Z", "is_active": True, "avatar_url": "https://example.com/avatars/john.jpg", }, ) ] ) class UserOutputSerializer(serializers.ModelSerializer): """User serializer for API responses.""" avatar_url = serializers.SerializerMethodField() class Meta: model = User fields = [ "id", "username", "email", "first_name", "last_name", "date_joined", "is_active", "avatar_url", ] read_only_fields = ["id", "date_joined", "is_active"] @extend_schema_field(serializers.URLField(allow_null=True)) def get_avatar_url(self, obj) -> str | None: """Get user avatar URL.""" if hasattr(obj, "profile") and obj.profile.avatar: return obj.profile.avatar.url return None class LoginInputSerializer(serializers.Serializer): """Input serializer for user login.""" username = serializers.CharField( max_length=254, help_text="Username or email address" ) password = serializers.CharField( max_length=128, style={"input_type": "password"}, trim_whitespace=False ) def validate(self, attrs): username = attrs.get("username") password = attrs.get("password") if username and password: return attrs raise serializers.ValidationError("Must include username/email and password.") class LoginOutputSerializer(serializers.Serializer): """Output serializer for successful login.""" token = serializers.CharField() user = UserOutputSerializer() message = serializers.CharField() class SignupInputSerializer(serializers.ModelSerializer): """Input serializer for user registration.""" password = serializers.CharField( write_only=True, validators=[validate_password], style={"input_type": "password"}, ) password_confirm = serializers.CharField( write_only=True, style={"input_type": "password"} ) class Meta: model = User fields = [ "username", "email", "first_name", "last_name", "password", "password_confirm", ] extra_kwargs = { "password": {"write_only": True}, "email": {"required": True}, } def validate_email(self, value): """Validate email is unique.""" if UserModel.objects.filter(email=value).exists(): raise serializers.ValidationError("A user with this email already exists.") return value def validate_username(self, value): """Validate username is unique.""" if UserModel.objects.filter(username=value).exists(): raise serializers.ValidationError( "A user with this username already exists." ) return value def validate(self, attrs): """Validate passwords match.""" password = attrs.get("password") password_confirm = attrs.get("password_confirm") if password != password_confirm: raise serializers.ValidationError( {"password_confirm": "Passwords do not match."} ) return attrs def create(self, validated_data): """Create user with validated data.""" validated_data.pop("password_confirm", None) password = validated_data.pop("password") # Use type: ignore for Django's create_user method which isn't properly typed user = UserModel.objects.create_user( # type: ignore[attr-defined] password=password, **validated_data ) return user class SignupOutputSerializer(serializers.Serializer): """Output serializer for successful signup.""" token = serializers.CharField() user = UserOutputSerializer() message = serializers.CharField() class PasswordResetInputSerializer(serializers.Serializer): """Input serializer for password reset request.""" email = serializers.EmailField() def validate_email(self, value): """Validate email exists.""" try: user = UserModel.objects.get(email=value) self.user = user return value except UserModel.DoesNotExist: # Don't reveal if email exists or not for security return value def save(self, **kwargs): """Send password reset email if user exists.""" if hasattr(self, "user"): # Create password reset token token = get_random_string(64) PasswordReset.objects.update_or_create( user=self.user, defaults={ "token": token, "expires_at": timezone.now() + timedelta(hours=24), "used": False, }, ) # Send reset email request = self.context.get("request") if request: site = get_current_site(request) reset_url = f"{request.scheme}://{site.domain}/reset-password/{token}/" context = { "user": self.user, "reset_url": reset_url, "site_name": site.name, } email_html = render_to_string( "accounts/email/password_reset.html", context ) EmailService.send_email( to=self.user.email, # type: ignore - Django user model has email subject="Reset your password", text=f"Click the link to reset your password: {reset_url}", site=site, html=email_html, ) class PasswordResetOutputSerializer(serializers.Serializer): """Output serializer for password reset request.""" detail = serializers.CharField() class PasswordChangeInputSerializer(serializers.Serializer): """Input serializer for password change.""" old_password = serializers.CharField( max_length=128, style={"input_type": "password"} ) new_password = serializers.CharField( max_length=128, validators=[validate_password], style={"input_type": "password"}, ) new_password_confirm = serializers.CharField( max_length=128, style={"input_type": "password"} ) def validate_old_password(self, value): """Validate old password is correct.""" user = self.context["request"].user if not user.check_password(value): raise serializers.ValidationError("Old password is incorrect.") return value def validate(self, attrs): """Validate new passwords match.""" new_password = attrs.get("new_password") new_password_confirm = attrs.get("new_password_confirm") if new_password != new_password_confirm: raise serializers.ValidationError( {"new_password_confirm": "New passwords do not match."} ) return attrs def save(self, **kwargs): """Change user password.""" user = self.context["request"].user # validated_data is guaranteed to exist after is_valid() is called new_password = self.validated_data["new_password"] # type: ignore[index] user.set_password(new_password) user.save() return user class PasswordChangeOutputSerializer(serializers.Serializer): """Output serializer for password change.""" detail = serializers.CharField() class LogoutOutputSerializer(serializers.Serializer): """Output serializer for logout.""" message = serializers.CharField() class SocialProviderOutputSerializer(serializers.Serializer): """Output serializer for social authentication providers.""" id = serializers.CharField() name = serializers.CharField() authUrl = serializers.URLField() class AuthStatusOutputSerializer(serializers.Serializer): """Output serializer for authentication status check.""" authenticated = serializers.BooleanField() user = UserOutputSerializer(allow_null=True) # === HEALTH CHECK SERIALIZERS === class HealthCheckOutputSerializer(serializers.Serializer): """Output serializer for health check responses.""" status = serializers.ChoiceField(choices=["healthy", "unhealthy"]) timestamp = serializers.DateTimeField() version = serializers.CharField() environment = serializers.CharField() response_time_ms = serializers.FloatField() checks = serializers.DictField() metrics = serializers.DictField() class PerformanceMetricsOutputSerializer(serializers.Serializer): """Output serializer for performance metrics.""" timestamp = serializers.DateTimeField() database_analysis = serializers.DictField() cache_performance = serializers.DictField() recent_slow_queries = serializers.ListField() class SimpleHealthOutputSerializer(serializers.Serializer): """Output serializer for simple health check.""" status = serializers.ChoiceField(choices=["ok", "error"]) timestamp = serializers.DateTimeField() error = serializers.CharField(required=False) # === HISTORY SERIALIZERS === class HistoryEventSerializer(serializers.Serializer): """Base serializer for history events from pghistory.""" pgh_id = serializers.IntegerField(read_only=True) pgh_created_at = serializers.DateTimeField(read_only=True) pgh_label = serializers.CharField(read_only=True) pgh_obj_id = serializers.IntegerField(read_only=True) pgh_context = serializers.JSONField(read_only=True, allow_null=True) pgh_diff = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_pgh_diff(self, obj) -> dict: """Get diff from previous version if available.""" if hasattr(obj, "diff_against_previous"): return obj.diff_against_previous() return {} class ParkHistoryEventSerializer(HistoryEventSerializer): """Serializer for Park history events.""" # Include all Park fields for complete history record name = serializers.CharField(read_only=True) slug = serializers.CharField(read_only=True) description = serializers.CharField(read_only=True) status = serializers.CharField(read_only=True) opening_date = serializers.DateField(read_only=True, allow_null=True) closing_date = serializers.DateField(read_only=True, allow_null=True) operating_season = serializers.CharField(read_only=True) size_acres = serializers.DecimalField( max_digits=10, decimal_places=2, read_only=True, allow_null=True ) website = serializers.URLField(read_only=True) average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, read_only=True, allow_null=True ) ride_count = serializers.IntegerField(read_only=True, allow_null=True) coaster_count = serializers.IntegerField(read_only=True, allow_null=True) class RideHistoryEventSerializer(HistoryEventSerializer): """Serializer for Ride history events.""" # Include all Ride fields for complete history record name = serializers.CharField(read_only=True) slug = serializers.CharField(read_only=True) description = serializers.CharField(read_only=True) category = serializers.CharField(read_only=True) status = serializers.CharField(read_only=True) post_closing_status = serializers.CharField(read_only=True, allow_null=True) opening_date = serializers.DateField(read_only=True, allow_null=True) closing_date = serializers.DateField(read_only=True, allow_null=True) status_since = serializers.DateField(read_only=True, allow_null=True) min_height_in = serializers.IntegerField(read_only=True, allow_null=True) max_height_in = serializers.IntegerField(read_only=True, allow_null=True) capacity_per_hour = serializers.IntegerField(read_only=True, allow_null=True) ride_duration_seconds = serializers.IntegerField(read_only=True, allow_null=True) average_rating = serializers.DecimalField( max_digits=3, decimal_places=2, read_only=True, allow_null=True ) class CompanyHistoryEventSerializer(HistoryEventSerializer): """Serializer for Company history events.""" name = serializers.CharField(read_only=True) slug = serializers.CharField(read_only=True) roles = serializers.ListField(child=serializers.CharField(), read_only=True) description = serializers.CharField(read_only=True) website = serializers.URLField(read_only=True) founded_year = serializers.IntegerField(read_only=True, allow_null=True) parks_count = serializers.IntegerField(read_only=True) rides_count = serializers.IntegerField(read_only=True) class HistorySummarySerializer(serializers.Serializer): """Summary serializer for history information.""" total_events = serializers.IntegerField() first_recorded = serializers.DateTimeField(allow_null=True) last_modified = serializers.DateTimeField(allow_null=True) major_changes_count = serializers.IntegerField() recent_changes = serializers.ListField( child=serializers.DictField(), allow_empty=True ) @extend_schema_serializer( examples=[ OpenApiExample( "Park History Example", summary="Example park history response", description="Complete history for a park including real-world changes", value={ "current": { "id": 1, "name": "Cedar Point", "slug": "cedar-point", "status": "OPERATING", }, "history_summary": { "total_events": 15, "first_recorded": "2020-01-15T10:00:00Z", "last_modified": "2024-08-20T14:30:00Z", "major_changes_count": 3, "recent_changes": [ { "field": "coaster_count", "old": "16", "new": "17", "date": "2024-08-20T14:30:00Z", } ], }, "events": [ { "pgh_id": 150, "pgh_created_at": "2024-08-20T14:30:00Z", "pgh_label": "park.update", "name": "Cedar Point", "coaster_count": 17, "pgh_diff": {"coaster_count": {"old": "16", "new": "17"}}, } ], }, ) ] ) class ParkHistoryOutputSerializer(serializers.Serializer): """Complete history output for parks including both version and real-world history.""" current = ParkDetailOutputSerializer() history_summary = HistorySummarySerializer() events = ParkHistoryEventSerializer(many=True) slug_history = serializers.ListField( child=serializers.DictField(), help_text="Historical slugs/names this park has had", ) @extend_schema_serializer( examples=[ OpenApiExample( "Ride History Example", summary="Example ride history response", description="Complete history for a ride including real-world changes", value={ "current": { "id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance", "status": "OPERATING", "park": {"id": 1, "name": "Cedar Point", "slug": "cedar-point"}, }, "history_summary": { "total_events": 8, "first_recorded": "2018-01-01T10:00:00Z", "last_modified": "2024-08-15T16:45:00Z", "major_changes_count": 2, "recent_changes": [ { "field": "status", "old": "CLOSED_TEMP", "new": "OPERATING", "date": "2024-08-15T16:45:00Z", } ], }, "events": [ { "pgh_id": 89, "pgh_created_at": "2024-08-15T16:45:00Z", "pgh_label": "ride.update", "name": "Steel Vengeance", "status": "OPERATING", "pgh_diff": { "status": {"old": "CLOSED_TEMP", "new": "OPERATING"} }, } ], }, ) ] ) class RideHistoryOutputSerializer(serializers.Serializer): """Complete history output for rides including both version and real-world history.""" current = RideDetailOutputSerializer() history_summary = HistorySummarySerializer() events = RideHistoryEventSerializer(many=True) slug_history = serializers.ListField( child=serializers.DictField(), help_text="Historical slugs/names this ride has had", ) class CompanyHistoryOutputSerializer(serializers.Serializer): """Complete history output for companies.""" current = CompanyOutputSerializer() history_summary = HistorySummarySerializer() events = CompanyHistoryEventSerializer(many=True) slug_history = serializers.ListField( child=serializers.DictField(), help_text="Historical slugs/names this company has had", ) class UnifiedHistoryEventSerializer(serializers.Serializer): """Unified serializer for events across all tracked models.""" pgh_id = serializers.IntegerField(read_only=True) pgh_created_at = serializers.DateTimeField(read_only=True) pgh_label = serializers.CharField(read_only=True) pgh_obj_id = serializers.IntegerField(read_only=True) pgh_obj_model = serializers.CharField(read_only=True) pgh_context = serializers.JSONField(read_only=True, allow_null=True) pgh_diff = serializers.JSONField(read_only=True) # Object identification object_name = serializers.CharField(read_only=True) object_slug = serializers.CharField(read_only=True, allow_null=True) # Change metadata change_type = serializers.SerializerMethodField() significance = serializers.SerializerMethodField() @extend_schema_field(serializers.CharField()) def get_change_type(self, obj) -> str: """Categorize the type of change.""" label = getattr(obj, "pgh_label", "") if "insert" in label or "create" in label: return "created" elif "update" in label or "change" in label: return "updated" elif "delete" in label: return "deleted" return "modified" @extend_schema_field(serializers.CharField()) def get_significance(self, obj) -> str: """Rate the significance of the change.""" diff = getattr(obj, "pgh_diff", {}) if not diff: return "minor" significant_fields = {"name", "status", "opening_date", "closing_date"} if any(field in diff for field in significant_fields): return "major" elif len(diff) > 3: return "moderate" return "minor" @extend_schema_serializer( examples=[ OpenApiExample( "Unified History Timeline Example", summary="Example unified history timeline", description="Timeline of all changes across parks, rides, and companies", value={ "count": 150, "results": [ { "pgh_id": 150, "pgh_created_at": "2024-08-20T14:30:00Z", "pgh_label": "park.update", "pgh_obj_model": "Park", "object_name": "Cedar Point", "object_slug": "cedar-point", "change_type": "updated", "significance": "moderate", "pgh_diff": {"coaster_count": {"old": "16", "new": "17"}}, }, { "pgh_id": 149, "pgh_created_at": "2024-08-19T09:15:00Z", "pgh_label": "ride.update", "pgh_obj_model": "Ride", "object_name": "Steel Vengeance", "object_slug": "steel-vengeance", "change_type": "updated", "significance": "major", "pgh_diff": { "status": {"old": "CLOSED_TEMP", "new": "OPERATING"} }, }, ], }, ) ] ) class UnifiedHistoryTimelineSerializer(serializers.Serializer): """Unified timeline of all changes across the platform.""" count = serializers.IntegerField() results = UnifiedHistoryEventSerializer(many=True)