diff --git a/backend/apps/api/v1/serializers_original_backup.py b/backend/apps/api/v1/serializers_original_backup.py deleted file mode 100644 index 2f796ae5..00000000 --- a/backend/apps/api/v1/serializers_original_backup.py +++ /dev/null @@ -1,2966 +0,0 @@ -""" -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.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 inside class methods to avoid Django initialization issues - -UserModel = get_user_model() - -# Define constants to avoid import-time model loading -CATEGORY_CHOICES = [ - ("RC", "Roller Coaster"), - ("FL", "Flat Ride"), - ("DR", "Dark Ride"), - ("WR", "Water Ride"), - ("TR", "Transport"), - ("OT", "Other"), -] - - -# Placeholder for dynamic model choices - will be populated at runtime -class ModelChoices: - @staticmethod - def get_ride_status_choices(): - try: - from apps.rides.models import Ride - - return Ride.STATUS_CHOICES - except ImportError: - return [("OPERATING", "Operating"), ("CLOSED", "Closed")] - - @staticmethod - def get_park_status_choices(): - try: - from apps.parks.models import Park - - return Park.STATUS_CHOICES - except ImportError: - return [("OPERATING", "Operating"), ("CLOSED", "Closed")] - - @staticmethod - def get_company_role_choices(): - try: - from apps.parks.models import Company - - return Company.CompanyRole.choices - except ImportError: - return [("OPERATOR", "Operator"), ("MANUFACTURER", "Manufacturer")] - - @staticmethod - def get_coaster_track_choices(): - try: - from apps.rides.models import RollerCoasterStats - - return RollerCoasterStats.TRACK_MATERIAL_CHOICES - except ImportError: - return [("STEEL", "Steel"), ("WOOD", "Wood")] - - @staticmethod - def get_coaster_type_choices(): - try: - from apps.rides.models import RollerCoasterStats - - return RollerCoasterStats.COASTER_TYPE_CHOICES - except ImportError: - return [("SITDOWN", "Sit Down"), ("INVERTED", "Inverted")] - - @staticmethod - def get_launch_choices(): - try: - from apps.rides.models import RollerCoasterStats - - return RollerCoasterStats.LAUNCH_CHOICES - except ImportError: - return [("CHAIN", "Chain Lift"), ("LAUNCH", "Launch")] - - @staticmethod - def get_top_list_categories(): - try: - from apps.accounts.models import TopList - - return TopList.Categories.choices - except ImportError: - return [("RC", "Roller Coasters"), ("PARKS", "Parks")] - - @staticmethod - def get_ride_post_closing_choices(): - try: - from apps.rides.models import Ride - - return Ride.POST_CLOSING_STATUS_CHOICES - except ImportError: - return [ - ("DEMOLISHED", "Demolished"), - ("RELOCATED", "Relocated"), - ("SBNO", "Standing But Not Operating"), - ] - - -# === 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=ModelChoices.get_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=ModelChoices.get_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=[], - required=False, # Choices set dynamically - ) - - # 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=[]) # 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", - ) - - -# === 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=ModelChoices.get_company_role_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) - - -# === EMAIL SERVICE SERIALIZERS === - - -@extend_schema_serializer( - examples=[ - OpenApiExample( - "Email Send Example", - summary="Example email send request", - description="Send an email through the ThrillWiki email service", - value={ - "to": "user@example.com", - "subject": "Welcome to ThrillWiki", - "text": "Thank you for joining ThrillWiki!", - "from_email": "noreply@thrillwiki.com", - }, - ) - ] -) -class EmailSendInputSerializer(serializers.Serializer): - """Input serializer for sending emails.""" - - to = serializers.EmailField(help_text="Recipient email address") - subject = serializers.CharField(max_length=255, help_text="Email subject line") - text = serializers.CharField(help_text="Email body text content") - from_email = serializers.EmailField( - required=False, - allow_blank=True, - help_text="Sender email address (optional, uses site default if not provided)", - ) - - def validate_to(self, value): - """Validate recipient email address.""" - if not value: - raise serializers.ValidationError("Recipient email is required") - return value - - def validate_subject(self, value): - """Validate email subject.""" - if not value.strip(): - raise serializers.ValidationError("Email subject cannot be empty") - return value.strip() - - def validate_text(self, value): - """Validate email content.""" - if not value.strip(): - raise serializers.ValidationError("Email content cannot be empty") - return value.strip() - - -class EmailSendOutputSerializer(serializers.Serializer): - """Output serializer for email send response.""" - - message = serializers.CharField() - response = serializers.JSONField(required=False) - - -# === CORE ENTITY SEARCH SERIALIZERS === - - -class EntityMatchSerializer(serializers.Serializer): - """Serializer for entity search matches.""" - - entity_type = serializers.CharField() - name = serializers.CharField() - slug = serializers.CharField() - score = serializers.FloatField() - confidence = serializers.CharField() - match_reason = serializers.CharField() - url = serializers.URLField() - entity_id = serializers.IntegerField() - - -class EntitySuggestionSerializer(serializers.Serializer): - """Serializer for entity creation suggestions.""" - - suggested_name = serializers.CharField() - entity_type = serializers.CharField() - requires_authentication = serializers.BooleanField() - login_prompt = serializers.CharField() - signup_prompt = serializers.CharField() - creation_hint = serializers.CharField() - - -@extend_schema_serializer( - examples=[ - OpenApiExample( - "Entity Search Request Example", - summary="Example entity search request", - description="Search for entities with fuzzy matching", - value={ - "query": "cedar point", - "entity_types": ["park", "ride"], - "include_suggestions": True, - }, - ) - ] -) -class EntitySearchInputSerializer(serializers.Serializer): - """Input serializer for entity fuzzy search.""" - - query = serializers.CharField( - min_length=2, max_length=255, help_text="Search query (minimum 2 characters)" - ) - entity_types = serializers.ListField( - child=serializers.ChoiceField(choices=["park", "ride", "company"]), - required=False, - default=["park", "ride", "company"], - help_text="Types of entities to search for", - ) - include_suggestions = serializers.BooleanField( - default=True, - help_text="Whether to include creation suggestions for missing entities", - ) - - def validate_query(self, value): - """Validate search query.""" - if len(value.strip()) < 2: - raise serializers.ValidationError( - "Query must be at least 2 characters long" - ) - return value.strip() - - -@extend_schema_serializer( - examples=[ - OpenApiExample( - "Entity Search Response Example", - summary="Example entity search response", - description="Successful entity search with matches and suggestions", - value={ - "success": True, - "query": "cedar point", - "matches": [ - { - "entity_type": "park", - "name": "Cedar Point", - "slug": "cedar-point", - "score": 0.95, - "confidence": "high", - "match_reason": "Exact name match", - "url": "/parks/cedar-point/", - "entity_id": 1, - } - ], - "suggestion": { - "suggested_name": "Cedar Point", - "entity_type": "park", - "requires_authentication": False, - "login_prompt": "Log in to suggest adding this park", - "signup_prompt": "Sign up to contribute to ThrillWiki", - "creation_hint": "Help expand our database", - }, - "user_authenticated": False, - }, - ) - ] -) -class EntitySearchOutputSerializer(serializers.Serializer): - """Output serializer for entity search results.""" - - success = serializers.BooleanField() - query = serializers.CharField() - matches = EntityMatchSerializer(many=True) - suggestion = EntitySuggestionSerializer(required=False, allow_null=True) - user_authenticated = serializers.BooleanField() - - -class EntitySuggestionItemSerializer(serializers.Serializer): - """Serializer for individual entity suggestions.""" - - name = serializers.CharField() - type = serializers.CharField() - slug = serializers.CharField() - url = serializers.URLField() - score = serializers.FloatField() - confidence = serializers.CharField() - - -@extend_schema_serializer( - examples=[ - OpenApiExample( - "Entity Suggestions Response Example", - summary="Example entity suggestions response", - description="Quick suggestions for autocomplete", - value={ - "suggestions": [ - { - "name": "Cedar Point", - "type": "park", - "slug": "cedar-point", - "url": "/parks/cedar-point/", - "score": 0.95, - "confidence": "high", - }, - { - "name": "Cedar Creek Mine Ride", - "type": "ride", - "slug": "cedar-creek-mine-ride", - "url": "/parks/cedar-point/rides/cedar-creek-mine-ride/", - "score": 0.85, - "confidence": "medium", - }, - ], - "query": "cedar", - "count": 2, - }, - ) - ] -) -class EntitySuggestionOutputSerializer(serializers.Serializer): - """Output serializer for entity suggestions.""" - - suggestions = EntitySuggestionItemSerializer(many=True) - query = serializers.CharField() - count = serializers.IntegerField() - error = serializers.CharField(required=False) - - -# === MAP SERVICE SERIALIZERS === - - -class MapLocationSerializer(serializers.Serializer): - """Serializer for map location data.""" - - id = serializers.IntegerField() - name = serializers.CharField() - type = serializers.CharField() - latitude = serializers.FloatField() - longitude = serializers.FloatField() - description = serializers.CharField(required=False) - url = serializers.URLField(required=False) - metadata = serializers.JSONField(required=False) - - -class MapClusterSerializer(serializers.Serializer): - """Serializer for map cluster data.""" - - latitude = serializers.FloatField() - longitude = serializers.FloatField() - count = serializers.IntegerField() - bounds = serializers.JSONField() - zoom_level = serializers.IntegerField() - - -@extend_schema_serializer( - examples=[ - OpenApiExample( - "Map Data Response Example", - summary="Example map data response", - description="Map locations with optional clustering", - value={ - "locations": [ - { - "id": 1, - "name": "Cedar Point", - "type": "park", - "latitude": 41.4793, - "longitude": -82.6833, - "description": "America's Roller Coast", - "url": "/parks/cedar-point/", - "metadata": {"status": "OPERATING", "coaster_count": 17}, - } - ], - "clusters": [ - { - "latitude": 41.5, - "longitude": -82.7, - "count": 5, - "bounds": { - "north": 41.6, - "south": 41.4, - "east": -82.6, - "west": -82.8, - }, - "zoom_level": 10, - } - ], - "clustered": True, - "cache_hit": False, - "query_time_ms": 45.2, - "filters_applied": ["park_status=OPERATING"], - }, - ) - ] -) -class MapDataOutputSerializer(serializers.Serializer): - """Output serializer for map data responses.""" - - locations = MapLocationSerializer(many=True, required=False) - clusters = MapClusterSerializer(many=True, required=False) - clustered = serializers.BooleanField() - cache_hit = serializers.BooleanField() - query_time_ms = serializers.FloatField() - filters_applied = serializers.ListField( - child=serializers.CharField(), required=False - ) - - -class MapBoundsInputSerializer(serializers.Serializer): - """Input serializer for map bounds queries.""" - - north = serializers.FloatField(min_value=-90, max_value=90) - south = serializers.FloatField(min_value=-90, max_value=90) - east = serializers.FloatField(min_value=-180, max_value=180) - west = serializers.FloatField(min_value=-180, max_value=180) - types = serializers.CharField( - required=False, help_text="Comma-separated location types" - ) - zoom = serializers.IntegerField( - min_value=1, max_value=20, required=False, default=10 - ) - - def validate(self, attrs): - """Validate bounds are logical.""" - north = attrs.get("north") - south = attrs.get("south") - east = attrs.get("east") - west = attrs.get("west") - - if north <= south: - raise serializers.ValidationError("North must be greater than south") - - if east <= west: - raise serializers.ValidationError("East must be greater than west") - - return attrs - - -class MapSearchInputSerializer(serializers.Serializer): - """Input serializer for map search queries.""" - - q = serializers.CharField( - min_length=2, max_length=255, help_text="Search query (minimum 2 characters)" - ) - north = serializers.FloatField(min_value=-90, max_value=90, required=False) - south = serializers.FloatField(min_value=-90, max_value=90, required=False) - east = serializers.FloatField(min_value=-180, max_value=180, required=False) - west = serializers.FloatField(min_value=-180, max_value=180, required=False) - types = serializers.CharField( - required=False, help_text="Comma-separated location types" - ) - limit = serializers.IntegerField( - min_value=1, max_value=500, required=False, default=50 - ) - - -class MapStatsOutputSerializer(serializers.Serializer): - """Output serializer for map service statistics.""" - - total_locations = serializers.IntegerField() - locations_by_type = serializers.JSONField() - cache_stats = serializers.JSONField() - performance_metrics = serializers.JSONField() - last_updated = serializers.DateTimeField() - - -class MapCacheInputSerializer(serializers.Serializer): - """Input serializer for map cache operations.""" - - location_type = serializers.CharField(required=False) - location_id = serializers.IntegerField(required=False) - bounds = serializers.JSONField(required=False) - - -# === MEDIA SERIALIZERS === - - -@extend_schema_serializer( - examples=[ - OpenApiExample( - "Photo Upload Example", - summary="Example photo upload request", - description="Upload a photo and associate it with a content object", - value={ - "photo": "binary_file_data", - "app_label": "parks", - "model": "park", - "object_id": 1, - "caption": "Beautiful view of the park entrance", - "alt_text": "Park entrance with fountain", - "is_primary": True, - }, - ) - ] -) -class PhotoUploadInputSerializer(serializers.Serializer): - """Input serializer for photo uploads.""" - - photo = serializers.ImageField(help_text="Image file to upload") - app_label = serializers.CharField( - max_length=100, help_text="App label of the content type (e.g., 'parks')" - ) - model = serializers.CharField( - max_length=100, help_text="Model name of the content type (e.g., 'park')" - ) - object_id = serializers.IntegerField( - help_text="ID of the object to associate the photo with" - ) - caption = serializers.CharField( - max_length=500, required=False, allow_blank=True, help_text="Photo caption" - ) - alt_text = serializers.CharField( - max_length=255, - required=False, - allow_blank=True, - help_text="Alternative text for accessibility", - ) - is_primary = serializers.BooleanField( - default=False, help_text="Whether this should be the primary photo" - ) - - def validate_photo(self, value): - """Validate uploaded photo.""" - if not value: - raise serializers.ValidationError("Photo file is required") - - # Check file size (10MB limit) - if value.size > 10 * 1024 * 1024: - raise serializers.ValidationError("Photo file size cannot exceed 10MB") - - # Check file type - allowed_types = ["image/jpeg", "image/png", "image/webp"] - if hasattr(value, "content_type") and value.content_type not in allowed_types: - raise serializers.ValidationError( - "Only JPEG, PNG, and WebP images are allowed" - ) - - return value - - -class PhotoUploadOutputSerializer(serializers.Serializer): - """Output serializer for photo upload response.""" - - id = serializers.IntegerField() - url = serializers.URLField() - caption = serializers.CharField() - alt_text = serializers.CharField() - is_primary = serializers.BooleanField() - message = serializers.CharField() - - -@extend_schema_serializer( - examples=[ - OpenApiExample( - "Photo Detail Example", - summary="Example photo detail response", - description="Detailed information about a photo", - value={ - "id": 1, - "url": "/media/photos/park_entrance.jpg", - "thumbnail_url": "/media/photos/thumbnails/park_entrance_thumb.jpg", - "caption": "Beautiful view of the park entrance", - "alt_text": "Park entrance with fountain", - "is_primary": True, - "content_type": "parks.park", - "object_id": 1, - "uploaded_by": { - "id": 1, - "username": "photographer", - "display_name": "Park Photographer", - }, - "uploaded_at": "2024-01-15T10:30:00Z", - "file_size": 2048576, - "dimensions": {"width": 1920, "height": 1080}, - }, - ) - ] -) -class PhotoDetailOutputSerializer(serializers.Serializer): - """Output serializer for photo details.""" - - id = serializers.IntegerField() - url = serializers.URLField() - thumbnail_url = serializers.URLField(required=False) - caption = serializers.CharField() - alt_text = serializers.CharField() - is_primary = serializers.BooleanField() - content_type = serializers.CharField() - object_id = serializers.IntegerField() - uploaded_by = serializers.SerializerMethodField() - uploaded_at = serializers.DateTimeField() - file_size = serializers.IntegerField() - dimensions = serializers.JSONField(required=False) - - @extend_schema_field(serializers.DictField()) - def get_uploaded_by(self, obj) -> dict: - """Get uploader information.""" - return { - "id": obj.uploaded_by.id, - "username": obj.uploaded_by.username, - "display_name": getattr( - obj.uploaded_by, "get_display_name", lambda: obj.uploaded_by.username - )(), - } - - -class PhotoListOutputSerializer(serializers.Serializer): - """Output serializer for photo list view.""" - - id = serializers.IntegerField() - url = serializers.URLField() - thumbnail_url = serializers.URLField(required=False) - caption = serializers.CharField() - is_primary = serializers.BooleanField() - uploaded_at = serializers.DateTimeField() - uploaded_by = serializers.SerializerMethodField() - - @extend_schema_field(serializers.DictField()) - def get_uploaded_by(self, obj) -> dict: - """Get uploader information.""" - return { - "id": obj.uploaded_by.id, - "username": obj.uploaded_by.username, - } - - -class PhotoUpdateInputSerializer(serializers.Serializer): - """Input serializer for updating photos.""" - - caption = serializers.CharField(max_length=500, required=False, allow_blank=True) - alt_text = serializers.CharField(max_length=255, required=False, allow_blank=True) - is_primary = serializers.BooleanField(required=False) - - -# === MODERATION SERIALIZERS === - - -class ModerationSubmissionSerializer(serializers.Serializer): - """Serializer for moderation submissions.""" - - submission_type = serializers.ChoiceField( - choices=["EDIT", "PHOTO", "REVIEW"], help_text="Type of submission" - ) - content_type = serializers.CharField(help_text="Content type being modified") - object_id = serializers.IntegerField(help_text="ID of object being modified") - changes = serializers.JSONField(help_text="Changes being submitted") - reason = serializers.CharField( - max_length=500, - required=False, - allow_blank=True, - help_text="Reason for the changes", - ) - - -class ModerationSubmissionOutputSerializer(serializers.Serializer): - """Output serializer for moderation submission responses.""" - - status = serializers.CharField() - message = serializers.CharField() - submission_id = serializers.IntegerField(required=False) - auto_approved = serializers.BooleanField(required=False) - - -# === PARKS SEARCH SERIALIZERS === - - -class ParkSuggestionSerializer(serializers.Serializer): - """Serializer for park search suggestions.""" - - id = serializers.IntegerField() - name = serializers.CharField() - slug = serializers.CharField() - location = serializers.CharField() - status = serializers.CharField() - coaster_count = serializers.IntegerField() - - -class ParkSuggestionOutputSerializer(serializers.Serializer): - """Output serializer for park suggestions.""" - - results = ParkSuggestionSerializer(many=True) - query = serializers.CharField() - count = serializers.IntegerField() - - -# === LOCATION SEARCH SERIALIZERS === - - -class LocationSearchResultSerializer(serializers.Serializer): - """Serializer for location search results.""" - - display_name = serializers.CharField() - lat = serializers.FloatField() - lon = serializers.FloatField() - type = serializers.CharField() - importance = serializers.FloatField() - address = serializers.JSONField() - - -class LocationSearchOutputSerializer(serializers.Serializer): - """Output serializer for location search.""" - - results = LocationSearchResultSerializer(many=True) - query = serializers.CharField() - count = serializers.IntegerField() - - -class ReverseGeocodeOutputSerializer(serializers.Serializer): - """Output serializer for reverse geocoding.""" - - display_name = serializers.CharField() - lat = serializers.FloatField() - lon = serializers.FloatField() - address = serializers.JSONField() - type = serializers.CharField() - - -# === ROADTRIP SERIALIZERS === - - -class RoadtripParkSerializer(serializers.Serializer): - """Serializer for parks in roadtrip planning.""" - - id = serializers.IntegerField() - name = serializers.CharField() - slug = serializers.CharField() - latitude = serializers.FloatField() - longitude = serializers.FloatField() - coaster_count = serializers.IntegerField() - status = serializers.CharField() - - -class RoadtripCreateInputSerializer(serializers.Serializer): - """Input serializer for creating roadtrips.""" - - name = serializers.CharField(max_length=255) - park_ids = serializers.ListField( - child=serializers.IntegerField(), - min_length=2, - max_length=10, - help_text="List of park IDs (2-10 parks)", - ) - start_date = serializers.DateField(required=False) - end_date = serializers.DateField(required=False) - notes = serializers.CharField(max_length=1000, required=False, allow_blank=True) - - def validate_park_ids(self, value): - """Validate park IDs.""" - if len(value) < 2: - raise serializers.ValidationError("At least 2 parks are required") - if len(value) > 10: - raise serializers.ValidationError("Maximum 10 parks allowed") - if len(set(value)) != len(value): - raise serializers.ValidationError("Duplicate park IDs not allowed") - return value - - -class RoadtripOutputSerializer(serializers.Serializer): - """Output serializer for roadtrip responses.""" - - id = serializers.CharField() - name = serializers.CharField() - parks = RoadtripParkSerializer(many=True) - total_distance_miles = serializers.FloatField() - estimated_drive_time_hours = serializers.FloatField() - route_coordinates = serializers.ListField( - child=serializers.ListField(child=serializers.FloatField()) - ) - created_at = serializers.DateTimeField() - - -class GeocodeInputSerializer(serializers.Serializer): - """Input serializer for geocoding requests.""" - - address = serializers.CharField(max_length=500, help_text="Address to geocode") - - -class GeocodeOutputSerializer(serializers.Serializer): - """Output serializer for geocoding responses.""" - - status = serializers.CharField() - coordinates = serializers.JSONField(required=False) - formatted_address = serializers.CharField(required=False) - message = serializers.CharField(required=False) - - -class DistanceCalculationInputSerializer(serializers.Serializer): - """Input serializer for distance calculations.""" - - park1_id = serializers.IntegerField() - park2_id = serializers.IntegerField() - - -class DistanceCalculationOutputSerializer(serializers.Serializer): - """Output serializer for distance calculations.""" - - status = serializers.CharField() - distance_miles = serializers.FloatField(required=False) - drive_time_hours = serializers.FloatField(required=False) - route_coordinates = serializers.ListField( - child=serializers.ListField(child=serializers.FloatField()), required=False - ) - message = serializers.CharField(required=False)