Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-08-26 13:19:04 -04:00
parent bf7e0c0f40
commit 831be6a2ee
151 changed files with 16260 additions and 9137 deletions

View File

@@ -0,0 +1,294 @@
"""
ThrillWiki API v1 serializers module.
This module provides a unified interface to all serializers across different domains
while maintaining the modular structure for better organization and maintainability.
"""
# Shared utilities and base classes
from .shared import (
CATEGORY_CHOICES,
ModelChoices,
LocationOutputSerializer,
CompanyOutputSerializer,
UserModel,
)
# Parks domain
from .parks import (
ParkListOutputSerializer,
ParkDetailOutputSerializer,
ParkCreateInputSerializer,
ParkUpdateInputSerializer,
ParkFilterInputSerializer,
ParkAreaDetailOutputSerializer,
ParkAreaCreateInputSerializer,
ParkAreaUpdateInputSerializer,
ParkLocationOutputSerializer,
ParkLocationCreateInputSerializer,
ParkLocationUpdateInputSerializer,
ParkSuggestionSerializer,
ParkSuggestionOutputSerializer,
)
# Companies and ride models domain
from .companies import (
CompanyDetailOutputSerializer,
CompanyCreateInputSerializer,
CompanyUpdateInputSerializer,
RideModelDetailOutputSerializer,
RideModelCreateInputSerializer,
RideModelUpdateInputSerializer,
)
# Rides domain
from .rides import (
RideParkOutputSerializer,
RideModelOutputSerializer,
RideListOutputSerializer,
RideDetailOutputSerializer,
RideCreateInputSerializer,
RideUpdateInputSerializer,
RideFilterInputSerializer,
RollerCoasterStatsOutputSerializer,
RollerCoasterStatsCreateInputSerializer,
RollerCoasterStatsUpdateInputSerializer,
RideLocationOutputSerializer,
RideLocationCreateInputSerializer,
RideLocationUpdateInputSerializer,
RideReviewOutputSerializer,
RideReviewCreateInputSerializer,
RideReviewUpdateInputSerializer,
)
# Accounts domain
from .accounts import (
UserProfileOutputSerializer,
UserProfileCreateInputSerializer,
UserProfileUpdateInputSerializer,
TopListOutputSerializer,
TopListCreateInputSerializer,
TopListUpdateInputSerializer,
TopListItemOutputSerializer,
TopListItemCreateInputSerializer,
TopListItemUpdateInputSerializer,
UserOutputSerializer,
LoginInputSerializer,
LoginOutputSerializer,
SignupInputSerializer,
SignupOutputSerializer,
PasswordResetInputSerializer,
PasswordResetOutputSerializer,
PasswordChangeInputSerializer,
PasswordChangeOutputSerializer,
LogoutOutputSerializer,
SocialProviderOutputSerializer,
AuthStatusOutputSerializer,
)
# Statistics and health checks
from .other import (
ParkStatsOutputSerializer,
RideStatsOutputSerializer,
ParkReviewOutputSerializer,
HealthCheckOutputSerializer,
PerformanceMetricsOutputSerializer,
SimpleHealthOutputSerializer,
)
# Media domain
from .media import (
PhotoUploadInputSerializer,
PhotoDetailOutputSerializer,
PhotoListOutputSerializer,
PhotoUpdateInputSerializer,
)
# Parks media domain
from .parks_media import (
ParkPhotoOutputSerializer,
ParkPhotoCreateInputSerializer,
ParkPhotoUpdateInputSerializer,
ParkPhotoListOutputSerializer,
ParkPhotoApprovalInputSerializer,
ParkPhotoStatsOutputSerializer,
)
# Rides media domain
from .rides_media import (
RidePhotoOutputSerializer,
RidePhotoCreateInputSerializer,
RidePhotoUpdateInputSerializer,
RidePhotoListOutputSerializer,
RidePhotoApprovalInputSerializer,
RidePhotoStatsOutputSerializer,
RidePhotoTypeFilterSerializer,
)
# Search domain
from .search import (
EntitySearchInputSerializer,
EntitySearchResultSerializer,
EntitySearchOutputSerializer,
LocationSearchResultSerializer,
LocationSearchOutputSerializer,
ReverseGeocodeOutputSerializer,
)
# History domain
from .history import (
ParkHistoryEventSerializer,
RideHistoryEventSerializer,
ParkHistoryOutputSerializer,
RideHistoryOutputSerializer,
UnifiedHistoryTimelineSerializer,
HistorySummarySerializer,
)
# Services domain
from .services import (
EmailSendInputSerializer,
EmailTemplateOutputSerializer,
MapDataOutputSerializer,
CoordinateInputSerializer,
HistoryEventSerializer,
HistoryEntryOutputSerializer,
HistoryCreateInputSerializer,
ModerationSubmissionSerializer,
ModerationSubmissionOutputSerializer,
RoadtripParkSerializer,
RoadtripCreateInputSerializer,
RoadtripOutputSerializer,
GeocodeInputSerializer,
GeocodeOutputSerializer,
DistanceCalculationInputSerializer,
DistanceCalculationOutputSerializer,
)
# Re-export everything for backward compatibility
__all__ = [
# Shared
"CATEGORY_CHOICES",
"ModelChoices",
"LocationOutputSerializer",
"CompanyOutputSerializer",
"UserModel",
# Parks
"ParkListOutputSerializer",
"ParkDetailOutputSerializer",
"ParkCreateInputSerializer",
"ParkUpdateInputSerializer",
"ParkFilterInputSerializer",
"ParkAreaDetailOutputSerializer",
"ParkAreaCreateInputSerializer",
"ParkAreaUpdateInputSerializer",
"ParkLocationOutputSerializer",
"ParkLocationCreateInputSerializer",
"ParkLocationUpdateInputSerializer",
"ParkSuggestionSerializer",
"ParkSuggestionOutputSerializer",
# Companies
"CompanyDetailOutputSerializer",
"CompanyCreateInputSerializer",
"CompanyUpdateInputSerializer",
"RideModelDetailOutputSerializer",
"RideModelCreateInputSerializer",
"RideModelUpdateInputSerializer",
# Rides
"RideParkOutputSerializer",
"RideModelOutputSerializer",
"RideListOutputSerializer",
"RideDetailOutputSerializer",
"RideCreateInputSerializer",
"RideUpdateInputSerializer",
"RideFilterInputSerializer",
"RollerCoasterStatsOutputSerializer",
"RollerCoasterStatsCreateInputSerializer",
"RollerCoasterStatsUpdateInputSerializer",
"RideLocationOutputSerializer",
"RideLocationCreateInputSerializer",
"RideLocationUpdateInputSerializer",
"RideReviewOutputSerializer",
"RideReviewCreateInputSerializer",
"RideReviewUpdateInputSerializer",
# Services
"EmailSendInputSerializer",
"EmailTemplateOutputSerializer",
"MapDataOutputSerializer",
"CoordinateInputSerializer",
"HistoryEventSerializer",
"HistoryEntryOutputSerializer",
"HistoryCreateInputSerializer",
"ModerationSubmissionSerializer",
"ModerationSubmissionOutputSerializer",
"RoadtripParkSerializer",
"RoadtripCreateInputSerializer",
"RoadtripOutputSerializer",
"GeocodeInputSerializer",
"GeocodeOutputSerializer",
"DistanceCalculationInputSerializer",
"DistanceCalculationOutputSerializer",
# Media
"PhotoUploadInputSerializer",
"PhotoDetailOutputSerializer",
"PhotoListOutputSerializer",
"PhotoUpdateInputSerializer",
# Parks Media
"ParkPhotoOutputSerializer",
"ParkPhotoCreateInputSerializer",
"ParkPhotoUpdateInputSerializer",
"ParkPhotoListOutputSerializer",
"ParkPhotoApprovalInputSerializer",
"ParkPhotoStatsOutputSerializer",
# Rides Media
"RidePhotoOutputSerializer",
"RidePhotoCreateInputSerializer",
"RidePhotoUpdateInputSerializer",
"RidePhotoListOutputSerializer",
"RidePhotoApprovalInputSerializer",
"RidePhotoStatsOutputSerializer",
"RidePhotoTypeFilterSerializer",
# Search
"EntitySearchInputSerializer",
"EntitySearchResultSerializer",
"EntitySearchOutputSerializer",
"LocationSearchResultSerializer",
"LocationSearchOutputSerializer",
"ReverseGeocodeOutputSerializer",
# History
"ParkHistoryEventSerializer",
"RideHistoryEventSerializer",
"ParkHistoryOutputSerializer",
"RideHistoryOutputSerializer",
"UnifiedHistoryTimelineSerializer",
"HistorySummarySerializer",
# Statistics and health
"ParkStatsOutputSerializer",
"RideStatsOutputSerializer",
"ParkReviewOutputSerializer",
"HealthCheckOutputSerializer",
"PerformanceMetricsOutputSerializer",
"SimpleHealthOutputSerializer",
# Accounts
"UserProfileOutputSerializer",
"UserProfileCreateInputSerializer",
"UserProfileUpdateInputSerializer",
"TopListOutputSerializer",
"TopListCreateInputSerializer",
"TopListUpdateInputSerializer",
"TopListItemOutputSerializer",
"TopListItemCreateInputSerializer",
"TopListItemUpdateInputSerializer",
"UserOutputSerializer",
"LoginInputSerializer",
"LoginOutputSerializer",
"SignupInputSerializer",
"SignupOutputSerializer",
"PasswordResetInputSerializer",
"PasswordResetOutputSerializer",
"PasswordChangeInputSerializer",
"PasswordChangeOutputSerializer",
"LogoutOutputSerializer",
"SocialProviderOutputSerializer",
"AuthStatusOutputSerializer",
]

View File

@@ -0,0 +1,496 @@
"""
Accounts domain serializers for ThrillWiki API v1.
This module contains all serializers related to user accounts, profiles,
authentication, top lists, and user statistics.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
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
from .shared import UserModel, ModelChoices
# === 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=ModelChoices.get_top_list_categories())
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=ModelChoices.get_top_list_categories(), 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)
# === 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 = UserModel
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 = UserModel
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)
# Note: PasswordReset model would need to be imported
# PasswordReset.objects.update_or_create(...)
pass
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)

View File

@@ -0,0 +1,149 @@
"""
Companies and ride models domain serializers for ThrillWiki API v1.
This module contains all serializers related to companies that operate parks
or manufacture rides, as well as ride model serializers.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
from .shared import CATEGORY_CHOICES, ModelChoices
# === COMPANY SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Company Example",
summary="Example company response",
description="A company that operates parks or manufactures rides",
value={
"id": 1,
"name": "Cedar Fair",
"slug": "cedar-fair",
"roles": ["OPERATOR", "PROPERTY_OWNER"],
"description": "Theme park operator based in Ohio",
"website": "https://cedarfair.com",
"founded_date": "1983-01-01",
"rides_count": 0,
"coasters_count": 0,
},
)
]
)
class CompanyDetailOutputSerializer(serializers.Serializer):
"""Output serializer for company details."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
roles = serializers.ListField(child=serializers.CharField())
description = serializers.CharField()
website = serializers.URLField()
founded_date = serializers.DateField(allow_null=True)
rides_count = serializers.IntegerField()
coasters_count = serializers.IntegerField()
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
class CompanyCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating companies."""
name = serializers.CharField(max_length=255)
roles = serializers.ListField(
child=serializers.ChoiceField(choices=ModelChoices.get_company_role_choices()),
allow_empty=False,
)
description = serializers.CharField(allow_blank=True, default="")
website = serializers.URLField(required=False, allow_blank=True)
founded_date = serializers.DateField(required=False, allow_null=True)
class CompanyUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating companies."""
name = serializers.CharField(max_length=255, required=False)
roles = serializers.ListField(
child=serializers.ChoiceField(choices=ModelChoices.get_company_role_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)

View File

@@ -0,0 +1,187 @@
"""
History domain serializers for ThrillWiki API v1.
This module contains serializers for history tracking and timeline functionality
using django-pghistory.
"""
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_serializer, extend_schema_field
import pghistory.models
class ParkHistoryEventSerializer(serializers.Serializer):
"""Serializer for park history events."""
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_data = serializers.JSONField(read_only=True)
event_type = serializers.SerializerMethodField()
changes = serializers.SerializerMethodField()
@extend_schema_field(serializers.CharField())
def get_event_type(self, obj) -> str:
"""Get human-readable event type."""
return obj.pgh_label.replace("_", " ").title()
@extend_schema_field(serializers.DictField())
def get_changes(self, obj) -> dict:
"""Get changes made in this event."""
if hasattr(obj, "pgh_diff") and obj.pgh_diff:
return obj.pgh_diff
return {}
class RideHistoryEventSerializer(serializers.Serializer):
"""Serializer for ride history events."""
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_data = serializers.JSONField(read_only=True)
event_type = serializers.SerializerMethodField()
changes = serializers.SerializerMethodField()
@extend_schema_field(serializers.CharField())
def get_event_type(self, obj) -> str:
"""Get human-readable event type."""
return obj.pgh_label.replace("_", " ").title()
@extend_schema_field(serializers.DictField())
def get_changes(self, obj) -> dict:
"""Get changes made in this event."""
if hasattr(obj, "pgh_diff") and obj.pgh_diff:
return obj.pgh_diff
return {}
class HistorySummarySerializer(serializers.Serializer):
"""Serializer for history summary information."""
total_events = serializers.IntegerField()
first_recorded = serializers.DateTimeField(allow_null=True)
last_modified = serializers.DateTimeField(allow_null=True)
class ParkHistoryOutputSerializer(serializers.Serializer):
"""Output serializer for complete park history."""
park = serializers.SerializerMethodField()
current_state = serializers.SerializerMethodField()
summary = HistorySummarySerializer()
events = ParkHistoryEventSerializer(many=True)
@extend_schema_field(serializers.DictField())
def get_park(self, obj) -> dict:
"""Get basic park information."""
park = obj.get("park")
if park:
return {
"id": park.id,
"name": park.name,
"slug": park.slug,
"status": park.status,
}
return {}
@extend_schema_field(serializers.DictField())
def get_current_state(self, obj) -> dict:
"""Get current park state."""
park = obj.get("current_state")
if park:
return {
"id": park.id,
"name": park.name,
"slug": park.slug,
"status": park.status,
"opening_date": (
park.opening_date.isoformat()
if hasattr(park, "opening_date") and park.opening_date
else None
),
"coaster_count": getattr(park, "coaster_count", 0),
"ride_count": getattr(park, "ride_count", 0),
}
return {}
class RideHistoryOutputSerializer(serializers.Serializer):
"""Output serializer for complete ride history."""
ride = serializers.SerializerMethodField()
current_state = serializers.SerializerMethodField()
summary = HistorySummarySerializer()
events = RideHistoryEventSerializer(many=True)
@extend_schema_field(serializers.DictField())
def get_ride(self, obj) -> dict:
"""Get basic ride information."""
ride = obj.get("ride")
if ride:
return {
"id": ride.id,
"name": ride.name,
"slug": ride.slug,
"park_name": ride.park.name if hasattr(ride, "park") else None,
"status": getattr(ride, "status", "UNKNOWN"),
}
return {}
@extend_schema_field(serializers.DictField())
def get_current_state(self, obj) -> dict:
"""Get current ride state."""
ride = obj.get("current_state")
if ride:
return {
"id": ride.id,
"name": ride.name,
"slug": ride.slug,
"park_name": ride.park.name if hasattr(ride, "park") else None,
"status": getattr(ride, "status", "UNKNOWN"),
"opening_date": (
ride.opening_date.isoformat()
if hasattr(ride, "opening_date") and ride.opening_date
else None
),
"ride_type": getattr(ride, "ride_type", "Unknown"),
}
return {}
class UnifiedHistoryTimelineSerializer(serializers.Serializer):
"""Serializer for unified history timeline."""
summary = serializers.SerializerMethodField()
events = serializers.SerializerMethodField()
@extend_schema_field(serializers.DictField())
def get_summary(self, obj) -> dict:
"""Get timeline summary."""
return obj.get("summary", {})
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_events(self, obj) -> list:
"""Get timeline events."""
events = obj.get("events", [])
event_data = []
for event in events:
event_data.append(
{
"pgh_id": event.pgh_id,
"pgh_created_at": event.pgh_created_at,
"pgh_label": event.pgh_label,
"pgh_model": event.pgh_model,
"pgh_obj_id": event.pgh_obj_id,
"pgh_context": event.pgh_context,
"event_type": event.pgh_label.replace("_", " ").title(),
"model_type": event.pgh_model.split(".")[-1].title(),
}
)
return event_data

View File

@@ -0,0 +1,124 @@
"""
Media domain serializers for ThrillWiki API v1.
This module contains serializers for photo uploads, media management,
and related media functionality.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
# === MEDIA SERIALIZERS ===
class PhotoUploadInputSerializer(serializers.Serializer):
"""Input serializer for photo uploads."""
file = serializers.ImageField()
caption = serializers.CharField(
max_length=500,
required=False,
allow_blank=True,
help_text="Optional caption for the photo",
)
alt_text = serializers.CharField(
max_length=255,
required=False,
allow_blank=True,
help_text="Alt text for accessibility",
)
is_primary = serializers.BooleanField(
default=False, help_text="Whether this should be the primary photo"
)
@extend_schema_serializer(
examples=[
OpenApiExample(
"Photo Detail Example",
summary="Example photo detail response",
description="A photo with full details",
value={
"id": 1,
"url": "https://example.com/media/photos/ride123.jpg",
"thumbnail_url": "https://example.com/media/thumbnails/ride123_thumb.jpg",
"caption": "Amazing view of Steel Vengeance",
"alt_text": "Steel Vengeance roller coaster with blue sky",
"is_primary": True,
"uploaded_at": "2024-08-15T10:30:00Z",
"uploaded_by": {
"id": 1,
"username": "coaster_photographer",
"display_name": "Coaster Photographer",
},
"content_type": "Ride",
"object_id": 123,
},
)
]
)
class PhotoDetailOutputSerializer(serializers.Serializer):
"""Output serializer for photo details."""
id = serializers.IntegerField()
url = serializers.URLField()
thumbnail_url = serializers.URLField(required=False)
caption = serializers.CharField()
alt_text = serializers.CharField()
is_primary = serializers.BooleanField()
uploaded_at = serializers.DateTimeField()
content_type = serializers.CharField()
object_id = serializers.IntegerField()
# File metadata
file_size = serializers.IntegerField()
width = serializers.IntegerField()
height = serializers.IntegerField()
format = serializers.CharField()
# Uploader info
uploaded_by = serializers.SerializerMethodField()
@extend_schema_field(serializers.DictField())
def get_uploaded_by(self, obj) -> dict:
"""Get uploader information."""
return {
"id": obj.uploaded_by.id,
"username": obj.uploaded_by.username,
"display_name": getattr(
obj.uploaded_by, "get_display_name", lambda: obj.uploaded_by.username
)(),
}
class PhotoListOutputSerializer(serializers.Serializer):
"""Output serializer for photo list view."""
id = serializers.IntegerField()
url = serializers.URLField()
thumbnail_url = serializers.URLField(required=False)
caption = serializers.CharField()
is_primary = serializers.BooleanField()
uploaded_at = serializers.DateTimeField()
uploaded_by = serializers.SerializerMethodField()
@extend_schema_field(serializers.DictField())
def get_uploaded_by(self, obj) -> dict:
"""Get uploader information."""
return {
"id": obj.uploaded_by.id,
"username": obj.uploaded_by.username,
}
class PhotoUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating photos."""
caption = serializers.CharField(max_length=500, required=False, allow_blank=True)
alt_text = serializers.CharField(max_length=255, required=False, allow_blank=True)
is_primary = serializers.BooleanField(required=False)

View File

@@ -0,0 +1,118 @@
"""
Statistics, health check, and miscellaneous domain serializers for ThrillWiki API v1.
This module contains serializers for statistics, health checks, and other
miscellaneous functionality.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
# === 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()
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,
}
# === 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)

View File

@@ -0,0 +1,448 @@
"""
Parks domain serializers for ThrillWiki API v1.
This module contains all serializers related to parks, park areas, park locations,
and park search functionality.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
from .shared import LocationOutputSerializer, CompanyOutputSerializer, ModelChoices
# === PARK SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Park List Example",
summary="Example park list response",
description="A typical park in the list view",
value={
"id": 1,
"name": "Cedar Point",
"slug": "cedar-point",
"status": "OPERATING",
"description": "America's Roller Coast",
"average_rating": 4.5,
"coaster_count": 17,
"ride_count": 70,
"location": {
"city": "Sandusky",
"state": "Ohio",
"country": "United States",
},
"operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"},
},
)
]
)
class ParkListOutputSerializer(serializers.Serializer):
"""Output serializer for park list view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = serializers.CharField()
description = serializers.CharField()
# Statistics
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, allow_null=True
)
coaster_count = serializers.IntegerField(allow_null=True)
ride_count = serializers.IntegerField(allow_null=True)
# Location (simplified for list view)
location = LocationOutputSerializer(allow_null=True)
# Operator info
operator = CompanyOutputSerializer()
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
@extend_schema_serializer(
examples=[
OpenApiExample(
"Park Detail Example",
summary="Example park detail response",
description="A complete park detail response",
value={
"id": 1,
"name": "Cedar Point",
"slug": "cedar-point",
"status": "OPERATING",
"description": "America's Roller Coast",
"opening_date": "1870-01-01",
"website": "https://cedarpoint.com",
"size_acres": 364.0,
"average_rating": 4.5,
"coaster_count": 17,
"ride_count": 70,
"location": {
"latitude": 41.4793,
"longitude": -82.6833,
"city": "Sandusky",
"state": "Ohio",
"country": "United States",
},
"operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"},
},
)
]
)
class ParkDetailOutputSerializer(serializers.Serializer):
"""Output serializer for park detail view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = serializers.CharField()
description = serializers.CharField()
# Details
opening_date = serializers.DateField(allow_null=True)
closing_date = serializers.DateField(allow_null=True)
operating_season = serializers.CharField()
size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, allow_null=True
)
website = serializers.URLField()
# Statistics
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, allow_null=True
)
coaster_count = serializers.IntegerField(allow_null=True)
ride_count = serializers.IntegerField(allow_null=True)
# Location (full details)
location = LocationOutputSerializer(allow_null=True)
# Companies
operator = CompanyOutputSerializer()
property_owner = CompanyOutputSerializer(allow_null=True)
# Areas
areas = serializers.SerializerMethodField()
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_areas(self, obj):
"""Get simplified area information."""
if hasattr(obj, "areas"):
return [
{
"id": area.id,
"name": area.name,
"slug": area.slug,
"description": area.description,
}
for area in obj.areas.all()
]
return []
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
class ParkCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating parks."""
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
status = serializers.ChoiceField(
choices=ModelChoices.get_park_status_choices(), default="OPERATING"
)
# Optional details
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
operating_season = serializers.CharField(
max_length=255, required=False, allow_blank=True
)
size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, allow_null=True
)
website = serializers.URLField(required=False, allow_blank=True)
# Required operator
operator_id = serializers.IntegerField()
# Optional property owner
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
def validate(self, attrs):
"""Cross-field validation."""
opening_date = attrs.get("opening_date")
closing_date = attrs.get("closing_date")
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
return attrs
class ParkUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating parks."""
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
status = serializers.ChoiceField(
choices=ModelChoices.get_park_status_choices(), required=False
)
# Optional details
opening_date = serializers.DateField(required=False, allow_null=True)
closing_date = serializers.DateField(required=False, allow_null=True)
operating_season = serializers.CharField(
max_length=255, required=False, allow_blank=True
)
size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, allow_null=True
)
website = serializers.URLField(required=False, allow_blank=True)
# Companies
operator_id = serializers.IntegerField(required=False)
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
def validate(self, attrs):
"""Cross-field validation."""
opening_date = attrs.get("opening_date")
closing_date = attrs.get("closing_date")
if opening_date and closing_date and closing_date < opening_date:
raise serializers.ValidationError(
"Closing date cannot be before opening date"
)
return attrs
class ParkFilterInputSerializer(serializers.Serializer):
"""Input serializer for park filtering and search."""
# Search
search = serializers.CharField(required=False, allow_blank=True)
# Status filter
status = serializers.MultipleChoiceField(
choices=[], required=False # Choices set dynamically
)
# Location filters
country = serializers.CharField(required=False, allow_blank=True)
state = serializers.CharField(required=False, allow_blank=True)
city = serializers.CharField(required=False, allow_blank=True)
# Rating filter
min_rating = serializers.DecimalField(
max_digits=3,
decimal_places=2,
required=False,
min_value=1,
max_value=10,
)
# Size filter
min_size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, min_value=0
)
max_size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, required=False, min_value=0
)
# Company filters
operator_id = serializers.IntegerField(required=False)
property_owner_id = serializers.IntegerField(required=False)
# Ordering
ordering = serializers.ChoiceField(
choices=[
"name",
"-name",
"opening_date",
"-opening_date",
"average_rating",
"-average_rating",
"coaster_count",
"-coaster_count",
"created_at",
"-created_at",
],
required=False,
default="name",
)
# === 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)
# === PARKS SEARCH SERIALIZERS ===
class ParkSuggestionSerializer(serializers.Serializer):
"""Serializer for park search suggestions."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
location = serializers.CharField()
status = serializers.CharField()
coaster_count = serializers.IntegerField()
class ParkSuggestionOutputSerializer(serializers.Serializer):
"""Output serializer for park suggestions."""
results = ParkSuggestionSerializer(many=True)
query = serializers.CharField()
count = serializers.IntegerField()

View File

@@ -0,0 +1,116 @@
"""
Park media serializers for ThrillWiki API.
This module contains serializers for park-specific media functionality.
"""
from rest_framework import serializers
from apps.parks.models import ParkPhoto
class ParkPhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for park photos."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
file_size = serializers.ReadOnlyField()
dimensions = serializers.ReadOnlyField()
park_slug = serializers.CharField(source='park.slug', read_only=True)
park_name = serializers.CharField(source='park.name', read_only=True)
class Meta:
model = ParkPhoto
fields = [
'id',
'image',
'caption',
'alt_text',
'is_primary',
'is_approved',
'created_at',
'updated_at',
'date_taken',
'uploaded_by_username',
'file_size',
'dimensions',
'park_slug',
'park_name',
]
read_only_fields = [
'id',
'created_at',
'updated_at',
'uploaded_by_username',
'file_size',
'dimensions',
'park_slug',
'park_name',
]
class ParkPhotoCreateInputSerializer(serializers.ModelSerializer):
"""Input serializer for creating park photos."""
class Meta:
model = ParkPhoto
fields = [
'image',
'caption',
'alt_text',
'is_primary',
]
class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer):
"""Input serializer for updating park photos."""
class Meta:
model = ParkPhoto
fields = [
'caption',
'alt_text',
'is_primary',
]
class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for park photo lists."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
class Meta:
model = ParkPhoto
fields = [
'id',
'image',
'caption',
'is_primary',
'is_approved',
'created_at',
'uploaded_by_username',
]
read_only_fields = fields
class ParkPhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for photo approval operations."""
photo_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of photo IDs to approve"
)
approve = serializers.BooleanField(
default=True,
help_text="Whether to approve (True) or reject (False) the photos"
)
class ParkPhotoStatsOutputSerializer(serializers.Serializer):
"""Output serializer for park photo statistics."""
total_photos = serializers.IntegerField()
approved_photos = serializers.IntegerField()
pending_photos = serializers.IntegerField()
has_primary = serializers.BooleanField()
recent_uploads = serializers.IntegerField()

View File

@@ -0,0 +1,651 @@
"""
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

View File

@@ -0,0 +1,147 @@
"""
Ride media serializers for ThrillWiki API.
This module contains serializers for ride-specific media functionality.
"""
from rest_framework import serializers
from apps.rides.models import RidePhoto
class RidePhotoOutputSerializer(serializers.ModelSerializer):
"""Output serializer for ride photos."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
file_size = serializers.ReadOnlyField()
dimensions = serializers.ReadOnlyField()
ride_slug = serializers.CharField(source='ride.slug', read_only=True)
ride_name = serializers.CharField(source='ride.name', read_only=True)
park_slug = serializers.CharField(source='ride.park.slug', read_only=True)
park_name = serializers.CharField(source='ride.park.name', read_only=True)
class Meta:
model = RidePhoto
fields = [
'id',
'image',
'caption',
'alt_text',
'is_primary',
'is_approved',
'photo_type',
'created_at',
'updated_at',
'date_taken',
'uploaded_by_username',
'file_size',
'dimensions',
'ride_slug',
'ride_name',
'park_slug',
'park_name',
]
read_only_fields = [
'id',
'created_at',
'updated_at',
'uploaded_by_username',
'file_size',
'dimensions',
'ride_slug',
'ride_name',
'park_slug',
'park_name',
]
class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
"""Input serializer for creating ride photos."""
class Meta:
model = RidePhoto
fields = [
'image',
'caption',
'alt_text',
'photo_type',
'is_primary',
]
class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
"""Input serializer for updating ride photos."""
class Meta:
model = RidePhoto
fields = [
'caption',
'alt_text',
'photo_type',
'is_primary',
]
class RidePhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for ride photo lists."""
uploaded_by_username = serializers.CharField(
source='uploaded_by.username', read_only=True)
class Meta:
model = RidePhoto
fields = [
'id',
'image',
'caption',
'photo_type',
'is_primary',
'is_approved',
'created_at',
'uploaded_by_username',
]
read_only_fields = fields
class RidePhotoApprovalInputSerializer(serializers.Serializer):
"""Input serializer for photo approval operations."""
photo_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of photo IDs to approve"
)
approve = serializers.BooleanField(
default=True,
help_text="Whether to approve (True) or reject (False) the photos"
)
class RidePhotoStatsOutputSerializer(serializers.Serializer):
"""Output serializer for ride photo statistics."""
total_photos = serializers.IntegerField()
approved_photos = serializers.IntegerField()
pending_photos = serializers.IntegerField()
has_primary = serializers.BooleanField()
recent_uploads = serializers.IntegerField()
by_type = serializers.DictField(
child=serializers.IntegerField(),
help_text="Photo counts by type"
)
class RidePhotoTypeFilterSerializer(serializers.Serializer):
"""Serializer for filtering photos by type."""
photo_type = serializers.ChoiceField(
choices=[
('exterior', 'Exterior View'),
('queue', 'Queue Area'),
('station', 'Station'),
('onride', 'On-Ride'),
('construction', 'Construction'),
('other', 'Other'),
],
required=False,
help_text="Filter photos by type"
)

View File

@@ -0,0 +1,88 @@
"""
Search domain serializers for ThrillWiki API v1.
This module contains serializers for entity search, location search,
and other search functionality.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
# === CORE ENTITY SEARCH SERIALIZERS ===
class EntitySearchInputSerializer(serializers.Serializer):
"""Input serializer for entity search requests."""
query = serializers.CharField(max_length=255, help_text="Search query string")
entity_types = serializers.ListField(
child=serializers.ChoiceField(choices=["park", "ride", "company", "user"]),
required=False,
help_text="Types of entities to search for",
)
limit = serializers.IntegerField(
default=10,
min_value=1,
max_value=50,
help_text="Maximum number of results to return",
)
class EntitySearchResultSerializer(serializers.Serializer):
"""Serializer for individual entity search results."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
type = serializers.CharField()
description = serializers.CharField()
relevance_score = serializers.FloatField()
# Context-specific info
context = serializers.JSONField(help_text="Additional context based on entity type")
class EntitySearchOutputSerializer(serializers.Serializer):
"""Output serializer for entity search results."""
query = serializers.CharField()
total_results = serializers.IntegerField()
results = EntitySearchResultSerializer(many=True)
search_time_ms = serializers.FloatField()
# === LOCATION SEARCH SERIALIZERS ===
class LocationSearchResultSerializer(serializers.Serializer):
"""Serializer for location search results."""
display_name = serializers.CharField()
lat = serializers.FloatField()
lon = serializers.FloatField()
type = serializers.CharField()
importance = serializers.FloatField()
address = serializers.JSONField()
class LocationSearchOutputSerializer(serializers.Serializer):
"""Output serializer for location search."""
results = LocationSearchResultSerializer(many=True)
query = serializers.CharField()
count = serializers.IntegerField()
class ReverseGeocodeOutputSerializer(serializers.Serializer):
"""Output serializer for reverse geocoding."""
display_name = serializers.CharField()
lat = serializers.FloatField()
lon = serializers.FloatField()
address = serializers.JSONField()
type = serializers.CharField()

View File

@@ -0,0 +1,229 @@
"""
Services domain serializers for ThrillWiki API v1.
This module contains serializers for various services like email, maps,
history tracking, moderation, and roadtrip planning.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
# === EMAIL SERVICE SERIALIZERS ===
class EmailSendInputSerializer(serializers.Serializer):
"""Input serializer for sending emails."""
to = serializers.EmailField()
subject = serializers.CharField(max_length=255)
text = serializers.CharField()
html = serializers.CharField(required=False)
template = serializers.CharField(required=False)
context = serializers.JSONField(required=False)
class EmailTemplateOutputSerializer(serializers.Serializer):
"""Output serializer for email templates."""
id = serializers.CharField()
name = serializers.CharField()
subject = serializers.CharField()
text_template = serializers.CharField()
html_template = serializers.CharField(required=False)
# === MAP SERVICE SERIALIZERS ===
class MapDataOutputSerializer(serializers.Serializer):
"""Output serializer for map data."""
parks = serializers.ListField(child=serializers.DictField())
rides = serializers.ListField(child=serializers.DictField())
bounds = serializers.DictField()
zoom_level = serializers.IntegerField()
class CoordinateInputSerializer(serializers.Serializer):
"""Input serializer for coordinate-based requests."""
latitude = serializers.FloatField(min_value=-90, max_value=90)
longitude = serializers.FloatField(min_value=-180, max_value=180)
radius_km = serializers.FloatField(min_value=0, max_value=1000, default=10)
# === 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 HistoryEntryOutputSerializer(serializers.Serializer):
"""Output serializer for history entries."""
id = serializers.IntegerField()
model_type = serializers.CharField()
object_id = serializers.IntegerField()
object_name = serializers.CharField()
action = serializers.CharField()
changes = serializers.JSONField()
timestamp = serializers.DateTimeField()
user = serializers.SerializerMethodField()
@extend_schema_field(serializers.DictField(allow_null=True))
def get_user(self, obj) -> dict | None:
if hasattr(obj, "user") and obj.user:
return {
"id": obj.user.id,
"username": obj.user.username,
}
return None
class HistoryCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating history entries."""
action = serializers.CharField(max_length=50)
description = serializers.CharField(max_length=500)
metadata = serializers.JSONField(required=False)
# === MODERATION SERIALIZERS ===
class ModerationSubmissionSerializer(serializers.Serializer):
"""Serializer for moderation submissions."""
submission_type = serializers.ChoiceField(
choices=["EDIT", "PHOTO", "REVIEW"], help_text="Type of submission"
)
content_type = serializers.CharField(help_text="Content type being modified")
object_id = serializers.IntegerField(help_text="ID of object being modified")
changes = serializers.JSONField(help_text="Changes being submitted")
reason = serializers.CharField(
max_length=500,
required=False,
allow_blank=True,
help_text="Reason for the changes",
)
class ModerationSubmissionOutputSerializer(serializers.Serializer):
"""Output serializer for moderation submission responses."""
status = serializers.CharField()
message = serializers.CharField()
submission_id = serializers.IntegerField(required=False)
auto_approved = serializers.BooleanField(required=False)
# === ROADTRIP SERIALIZERS ===
class RoadtripParkSerializer(serializers.Serializer):
"""Serializer for parks in roadtrip planning."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
latitude = serializers.FloatField()
longitude = serializers.FloatField()
coaster_count = serializers.IntegerField()
status = serializers.CharField()
class RoadtripCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating roadtrips."""
name = serializers.CharField(max_length=255)
park_ids = serializers.ListField(
child=serializers.IntegerField(),
min_length=2,
max_length=10,
help_text="List of park IDs (2-10 parks)",
)
start_date = serializers.DateField(required=False)
end_date = serializers.DateField(required=False)
notes = serializers.CharField(max_length=1000, required=False, allow_blank=True)
def validate_park_ids(self, value):
"""Validate park IDs."""
if len(value) < 2:
raise serializers.ValidationError("At least 2 parks are required")
if len(value) > 10:
raise serializers.ValidationError("Maximum 10 parks allowed")
if len(set(value)) != len(value):
raise serializers.ValidationError("Duplicate park IDs not allowed")
return value
class RoadtripOutputSerializer(serializers.Serializer):
"""Output serializer for roadtrip responses."""
id = serializers.CharField()
name = serializers.CharField()
parks = RoadtripParkSerializer(many=True)
total_distance_miles = serializers.FloatField()
estimated_drive_time_hours = serializers.FloatField()
route_coordinates = serializers.ListField(
child=serializers.ListField(child=serializers.FloatField())
)
created_at = serializers.DateTimeField()
class GeocodeInputSerializer(serializers.Serializer):
"""Input serializer for geocoding requests."""
address = serializers.CharField(max_length=500, help_text="Address to geocode")
class GeocodeOutputSerializer(serializers.Serializer):
"""Output serializer for geocoding responses."""
status = serializers.CharField()
coordinates = serializers.JSONField(required=False)
formatted_address = serializers.CharField(required=False)
# === DISTANCE CALCULATION SERIALIZERS ===
class DistanceCalculationInputSerializer(serializers.Serializer):
"""Input serializer for distance calculation requests."""
park1_id = serializers.IntegerField(help_text="ID of first park")
park2_id = serializers.IntegerField(help_text="ID of second park")
def validate(self, data):
"""Validate that park IDs are different."""
if data["park1_id"] == data["park2_id"]:
raise serializers.ValidationError("Park IDs must be different")
return data
class DistanceCalculationOutputSerializer(serializers.Serializer):
"""Output serializer for distance calculation responses."""
status = serializers.CharField()
distance_miles = serializers.FloatField(required=False)
distance_km = serializers.FloatField(required=False)
drive_time_hours = serializers.FloatField(required=False)
message = serializers.CharField(required=False)

View File

@@ -0,0 +1,159 @@
"""
Shared serializers and utilities for ThrillWiki API v1.
This module contains common serializers and helper classes used across multiple domains
to avoid code duplication and maintain consistency.
"""
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from django.contrib.auth import get_user_model
# Import models inside class methods to avoid Django initialization issues
UserModel = get_user_model()
# Define constants to avoid import-time model loading
CATEGORY_CHOICES = [
("RC", "Roller Coaster"),
("FL", "Flat Ride"),
("DR", "Dark Ride"),
("WR", "Water Ride"),
("TR", "Transport"),
("OT", "Other"),
]
# Placeholder for dynamic model choices - will be populated at runtime
class ModelChoices:
@staticmethod
def get_ride_status_choices():
try:
from apps.rides.models import Ride
return Ride.STATUS_CHOICES
except ImportError:
return [("OPERATING", "Operating"), ("CLOSED", "Closed")]
@staticmethod
def get_park_status_choices():
try:
from apps.parks.models import Park
return Park.STATUS_CHOICES
except ImportError:
return [("OPERATING", "Operating"), ("CLOSED", "Closed")]
@staticmethod
def get_company_role_choices():
try:
from apps.parks.models import Company
return Company.CompanyRole.choices
except ImportError:
return [("OPERATOR", "Operator"), ("MANUFACTURER", "Manufacturer")]
@staticmethod
def get_coaster_track_choices():
try:
from apps.rides.models import RollerCoasterStats
return RollerCoasterStats.TRACK_MATERIAL_CHOICES
except ImportError:
return [("STEEL", "Steel"), ("WOOD", "Wood")]
@staticmethod
def get_coaster_type_choices():
try:
from apps.rides.models import RollerCoasterStats
return RollerCoasterStats.COASTER_TYPE_CHOICES
except ImportError:
return [("SITDOWN", "Sit Down"), ("INVERTED", "Inverted")]
@staticmethod
def get_launch_choices():
try:
from apps.rides.models import RollerCoasterStats
return RollerCoasterStats.LAUNCH_CHOICES
except ImportError:
return [("CHAIN", "Chain Lift"), ("LAUNCH", "Launch")]
@staticmethod
def get_top_list_categories():
try:
from apps.accounts.models import TopList
return TopList.Categories.choices
except ImportError:
return [("RC", "Roller Coasters"), ("PARKS", "Parks")]
@staticmethod
def get_ride_post_closing_choices():
try:
from apps.rides.models import Ride
return Ride.POST_CLOSING_STATUS_CHOICES
except ImportError:
return [
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
("SBNO", "Standing But Not Operating"),
]
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)