Files
thrillwiki_django_no_react/backend/apps/api/v1/serializers.py
pacnpal e62646bcf9 feat: major API restructure and Vue.js frontend integration
- Centralize API endpoints in dedicated api app with v1 versioning
- Remove individual API modules from parks and rides apps
- Add event tracking system with analytics functionality
- Integrate Vue.js frontend with Tailwind CSS v4 and TypeScript
- Add comprehensive database migrations for event tracking
- Implement user authentication and social provider setup
- Add API schema documentation and serializers
- Configure development environment with shared scripts
- Update project structure for monorepo with frontend/backend separation
2025-08-24 16:42:20 -04:00

2180 lines
74 KiB
Python

"""
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)