Files
thrillwiki_django_no_react/backend/apps/api/v1/serializers/rides.py

652 lines
22 KiB
Python

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