remove backend

This commit is contained in:
pacnpal
2025-09-21 20:19:12 -04:00
parent 9e724bd795
commit f3c59ad6ff
557 changed files with 1739 additions and 4836 deletions

View File

@@ -0,0 +1,948 @@
"""
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 config.django import base as settings
from .shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === RIDE SERIALIZERS ===
class RideParkOutputSerializer(serializers.Serializer):
"""Output serializer for ride's park data."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
url = serializers.SerializerMethodField()
@extend_schema_field(serializers.URLField())
def get_url(self, obj) -> str:
"""Generate the frontend URL for this park."""
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.slug}/"
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 = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="rides"
)
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)
# URL
url = serializers.SerializerMethodField()
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
@extend_schema_field(serializers.URLField())
def get_url(self, obj) -> str:
"""Generate the frontend URL for this ride."""
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.park.slug}/rides/{obj.slug}/"
@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",
},
"photos": [
{
"id": 123,
"image_url": "https://imagedelivery.net/account-hash/abc123def456/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/abc123def456/thumbnail",
"medium": "https://imagedelivery.net/account-hash/abc123def456/medium",
"large": "https://imagedelivery.net/account-hash/abc123def456/large",
"public": "https://imagedelivery.net/account-hash/abc123def456/public",
},
"caption": "Amazing roller coaster photo",
"is_primary": True,
"photo_type": "exterior",
}
],
"primary_photo": {
"id": 123,
"image_url": "https://imagedelivery.net/account-hash/abc123def456/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/abc123def456/thumbnail",
"medium": "https://imagedelivery.net/account-hash/abc123def456/medium",
"large": "https://imagedelivery.net/account-hash/abc123def456/large",
"public": "https://imagedelivery.net/account-hash/abc123def456/public",
},
"caption": "Amazing roller coaster photo",
"photo_type": "exterior",
},
},
)
]
)
class RideDetailOutputSerializer(serializers.Serializer):
"""Output serializer for ride detail view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="rides"
)
post_closing_status = RichChoiceFieldSerializer(
choice_group="post_closing_statuses",
domain="rides",
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)
# Photos
photos = serializers.SerializerMethodField()
primary_photo = serializers.SerializerMethodField()
banner_image = serializers.SerializerMethodField()
card_image = serializers.SerializerMethodField()
# URL
url = serializers.SerializerMethodField()
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
@extend_schema_field(serializers.URLField())
def get_url(self, obj) -> str:
"""Generate the frontend URL for this ride."""
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.park.slug}/rides/{obj.slug}/"
@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
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_photos(self, obj):
"""Get all approved photos for this ride."""
from apps.rides.models import RidePhoto
photos = RidePhoto.objects.filter(ride=obj, is_approved=True).order_by(
"-is_primary", "-created_at"
)[
:10
] # Limit to 10 photos
return [
{
"id": photo.id,
"image_url": photo.image.url if photo.image else None,
"image_variants": (
{
"thumbnail": (
f"{photo.image.url}/thumbnail" if photo.image else None
),
"medium": f"{photo.image.url}/medium" if photo.image else None,
"large": f"{photo.image.url}/large" if photo.image else None,
"public": f"{photo.image.url}/public" if photo.image else None,
}
if photo.image
else {}
),
"caption": photo.caption,
"alt_text": photo.alt_text,
"is_primary": photo.is_primary,
"photo_type": photo.photo_type,
}
for photo in photos
]
@extend_schema_field(serializers.DictField(allow_null=True))
def get_primary_photo(self, obj):
"""Get the primary photo for this ride."""
from apps.rides.models import RidePhoto
try:
photo = RidePhoto.objects.filter(
ride=obj, is_primary=True, is_approved=True
).first()
if photo and photo.image:
return {
"id": photo.id,
"image_url": photo.image.url,
"image_variants": {
"thumbnail": f"{photo.image.url}/thumbnail",
"medium": f"{photo.image.url}/medium",
"large": f"{photo.image.url}/large",
"public": f"{photo.image.url}/public",
},
"caption": photo.caption,
"alt_text": photo.alt_text,
"photo_type": photo.photo_type,
}
except Exception:
pass
return None
@extend_schema_field(serializers.DictField(allow_null=True))
def get_banner_image(self, obj):
"""Get the banner image for this ride with fallback to latest photo."""
# First try the explicitly set banner image
if obj.banner_image and obj.banner_image.image:
return {
"id": obj.banner_image.id,
"image_url": obj.banner_image.image.url,
"image_variants": {
"thumbnail": f"{obj.banner_image.image.url}/thumbnail",
"medium": f"{obj.banner_image.image.url}/medium",
"large": f"{obj.banner_image.image.url}/large",
"public": f"{obj.banner_image.image.url}/public",
},
"caption": obj.banner_image.caption,
"alt_text": obj.banner_image.alt_text,
"photo_type": obj.banner_image.photo_type,
}
# Fallback to latest approved photo
from apps.rides.models import RidePhoto
try:
latest_photo = (
RidePhoto.objects.filter(
ride=obj, is_approved=True, image__isnull=False
)
.order_by("-created_at")
.first()
)
if latest_photo and latest_photo.image:
return {
"id": latest_photo.id,
"image_url": latest_photo.image.url,
"image_variants": {
"thumbnail": f"{latest_photo.image.url}/thumbnail",
"medium": f"{latest_photo.image.url}/medium",
"large": f"{latest_photo.image.url}/large",
"public": f"{latest_photo.image.url}/public",
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
"photo_type": latest_photo.photo_type,
"is_fallback": True,
}
except Exception:
pass
return None
@extend_schema_field(serializers.DictField(allow_null=True))
def get_card_image(self, obj):
"""Get the card image for this ride with fallback to latest photo."""
# First try the explicitly set card image
if obj.card_image and obj.card_image.image:
return {
"id": obj.card_image.id,
"image_url": obj.card_image.image.url,
"image_variants": {
"thumbnail": f"{obj.card_image.image.url}/thumbnail",
"medium": f"{obj.card_image.image.url}/medium",
"large": f"{obj.card_image.image.url}/large",
"public": f"{obj.card_image.image.url}/public",
},
"caption": obj.card_image.caption,
"alt_text": obj.card_image.alt_text,
"photo_type": obj.card_image.photo_type,
}
# Fallback to latest approved photo
from apps.rides.models import RidePhoto
try:
latest_photo = (
RidePhoto.objects.filter(
ride=obj, is_approved=True, image__isnull=False
)
.order_by("-created_at")
.first()
)
if latest_photo and latest_photo.image:
return {
"id": latest_photo.id,
"image_url": latest_photo.image.url,
"image_variants": {
"thumbnail": f"{latest_photo.image.url}/thumbnail",
"medium": f"{latest_photo.image.url}/medium",
"large": f"{latest_photo.image.url}/large",
"public": f"{latest_photo.image.url}/public",
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
"photo_type": latest_photo.photo_type,
"is_fallback": True,
}
except Exception:
pass
return None
class RideImageSettingsInputSerializer(serializers.Serializer):
"""Input serializer for setting ride banner and card images."""
banner_image_id = serializers.IntegerField(required=False, allow_null=True)
card_image_id = serializers.IntegerField(required=False, allow_null=True)
def validate_banner_image_id(self, value):
"""Validate that the banner image belongs to the same ride."""
if value is not None:
from apps.rides.models import RidePhoto
try:
RidePhoto.objects.get(id=value)
# The ride will be validated in the view
return value
except RidePhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
return value
def validate_card_image_id(self, value):
"""Validate that the card image belongs to the same ride."""
if value is not None:
from apps.rides.models import RidePhoto
try:
RidePhoto.objects.get(id=value)
# The ride will be validated in the view
return value
except RidePhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
return value
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=ModelChoices.get_ride_category_choices())
status = serializers.ChoiceField(
choices=ModelChoices.get_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"
)
# Park area validation when park changes
park_id = attrs.get("park_id")
park_area_id = attrs.get("park_area_id")
if park_id and park_area_id:
try:
from apps.parks.models import ParkArea
park_area = ParkArea.objects.get(id=park_area_id)
if park_area.park_id != park_id:
raise serializers.ValidationError(
f"Park area '{park_area.name}' does not belong to the selected park"
)
except Exception:
# If models aren't available or area doesn't exist, let the view handle it
pass
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=ModelChoices.get_ride_category_choices(), required=False
)
status = serializers.ChoiceField(
choices=ModelChoices.get_ride_status_choices(), required=False
)
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=ModelChoices.get_ride_category_choices(), required=False
)
# Status filter
status = serializers.MultipleChoiceField(
choices=ModelChoices.get_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",
)
# === 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",
"propulsion_system": "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 = RichChoiceFieldSerializer(
choice_group="track_materials",
domain="rides"
)
roller_coaster_type = RichChoiceFieldSerializer(
choice_group="coaster_types",
domain="rides"
)
max_drop_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
propulsion_system = RichChoiceFieldSerializer(
choice_group="propulsion_systems",
domain="rides"
)
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
)
propulsion_system = serializers.ChoiceField(
choices=ModelChoices.get_propulsion_system_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
)
propulsion_system = serializers.ChoiceField(
choices=ModelChoices.get_propulsion_system_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