mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:51:10 -05:00
- 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
2180 lines
74 KiB
Python
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)
|