This commit is contained in:
pacnpal
2025-09-21 20:04:42 -04:00
parent 42a3dc7637
commit 75cc618c2b
610 changed files with 1719 additions and 4816 deletions

View File

@@ -1,330 +0,0 @@
"""
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.
"""
from .services import (
HealthCheckOutputSerializer,
PerformanceMetricsOutputSerializer,
SimpleHealthOutputSerializer,
EmailSendInputSerializer,
EmailTemplateOutputSerializer,
MapDataOutputSerializer,
CoordinateInputSerializer,
HistoryEventSerializer,
HistoryEntryOutputSerializer,
HistoryCreateInputSerializer,
ModerationSubmissionSerializer,
ModerationSubmissionOutputSerializer,
RoadtripParkSerializer,
RoadtripCreateInputSerializer,
RoadtripOutputSerializer,
GeocodeInputSerializer,
GeocodeOutputSerializer,
DistanceCalculationInputSerializer,
DistanceCalculationOutputSerializer,
) # noqa: F401
from typing import Any, Dict, List
import importlib
# --- Shared utilities and base classes ---
from .shared import (
FilterOptionSerializer,
FilterRangeSerializer,
StandardizedFilterMetadataSerializer,
validate_filter_metadata_contract,
ensure_filter_option_format,
) # noqa: F401
# --- Parks domain ---
from .parks import (
ParkListOutputSerializer,
ParkDetailOutputSerializer,
ParkCreateInputSerializer,
ParkUpdateInputSerializer,
ParkFilterInputSerializer,
ParkAreaDetailOutputSerializer,
ParkAreaCreateInputSerializer,
ParkAreaUpdateInputSerializer,
ParkLocationOutputSerializer,
ParkLocationCreateInputSerializer,
ParkLocationUpdateInputSerializer,
ParkSuggestionSerializer,
ParkSuggestionOutputSerializer,
) # noqa: F401
# --- Companies and ride models domain ---
from .companies import (
CompanyDetailOutputSerializer,
CompanyCreateInputSerializer,
CompanyUpdateInputSerializer,
RideModelDetailOutputSerializer,
RideModelCreateInputSerializer,
RideModelUpdateInputSerializer,
) # noqa: F401
# --- Rides domain ---
from .rides import (
RideParkOutputSerializer,
RideModelOutputSerializer,
RideListOutputSerializer,
RideDetailOutputSerializer,
RideCreateInputSerializer,
RideUpdateInputSerializer,
RideFilterInputSerializer,
RollerCoasterStatsOutputSerializer,
RollerCoasterStatsCreateInputSerializer,
RollerCoasterStatsUpdateInputSerializer,
RideLocationOutputSerializer,
RideLocationCreateInputSerializer,
RideLocationUpdateInputSerializer,
RideReviewOutputSerializer,
RideReviewCreateInputSerializer,
RideReviewUpdateInputSerializer,
) # noqa: F401
# --- Accounts domain: try multiple likely locations, fall back to placeholders ---
_ACCOUNTS_SYMBOLS: List[str] = [
"UserProfileOutputSerializer",
"UserProfileCreateInputSerializer",
"UserProfileUpdateInputSerializer",
"TopListOutputSerializer",
"TopListCreateInputSerializer",
"TopListUpdateInputSerializer",
"TopListItemOutputSerializer",
"TopListItemCreateInputSerializer",
"TopListItemUpdateInputSerializer",
"UserOutputSerializer",
"LoginInputSerializer",
"LoginOutputSerializer",
"SignupInputSerializer",
"SignupOutputSerializer",
"PasswordResetInputSerializer",
"PasswordResetOutputSerializer",
"PasswordChangeInputSerializer",
"PasswordChangeOutputSerializer",
"LogoutOutputSerializer",
"SocialProviderOutputSerializer",
"AuthStatusOutputSerializer",
]
def _import_accounts_symbols() -> Dict[str, Any]:
"""
Try a list of candidate module paths and return a dict mapping expected symbol
names to the objects found. If no candidate provides a symbol, the symbol maps to None.
"""
candidates = [
f"{__package__}.accounts",
f"{__package__}.auth",
"apps.accounts.serializers",
"apps.api.v1.auth.serializers",
]
# Prepare default placeholders
result: Dict[str, Any] = {name: None for name in _ACCOUNTS_SYMBOLS}
for modname in candidates:
try:
module = importlib.import_module(modname)
except Exception:
continue
# Fill in any symbols that exist on this module (don't require all)
for name in _ACCOUNTS_SYMBOLS:
if hasattr(module, name):
result[name] = getattr(module, name)
# If we've found at least one real object (not all None), stop trying further candidates.
if any(result[name] is not None for name in _ACCOUNTS_SYMBOLS):
break
return result
_accounts = _import_accounts_symbols()
# Bind account symbols into the module namespace (only if they exist)
for _name in _ACCOUNTS_SYMBOLS:
if _accounts.get(_name) is not None:
globals()[_name] = _accounts[_name]
# --- Services domain ---
# --- Optionally try importing other domain modules and inject serializer-like names ---
_optional_domains = [
"other",
"media",
"parks_media",
"rides_media",
"search",
"history",
]
for domain in _optional_domains:
modname = f"{__package__}.{domain}"
try:
module = importlib.import_module(modname)
except Exception:
continue
# Inject any attribute that looks like a serializer or matches uppercase naming used by exported symbols
for attr in dir(module):
if attr.startswith("_"):
continue
# Heuristic: export classes/constants that end with 'Serializer' or are uppercase constants
if (
attr.endswith("Serializer")
or attr.isupper()
or attr.endswith("OutputSerializer")
or attr.endswith("InputSerializer")
):
globals()[attr] = getattr(module, attr)
# --- Construct a conservative __all__ based on explicit lists and discovered serializer names ---
_SHARED_EXPORTS = [
"FilterOptionSerializer",
"FilterRangeSerializer",
"StandardizedFilterMetadataSerializer",
"validate_filter_metadata_contract",
"ensure_filter_option_format",
]
_PARKS_EXPORTS = [
"ParkListOutputSerializer",
"ParkDetailOutputSerializer",
"ParkCreateInputSerializer",
"ParkUpdateInputSerializer",
"ParkFilterInputSerializer",
"ParkAreaDetailOutputSerializer",
"ParkAreaCreateInputSerializer",
"ParkAreaUpdateInputSerializer",
"ParkLocationOutputSerializer",
"ParkLocationCreateInputSerializer",
"ParkLocationUpdateInputSerializer",
"ParkSuggestionSerializer",
"ParkSuggestionOutputSerializer",
]
_COMPANIES_EXPORTS = [
"CompanyDetailOutputSerializer",
"CompanyCreateInputSerializer",
"CompanyUpdateInputSerializer",
"RideModelDetailOutputSerializer",
"RideModelCreateInputSerializer",
"RideModelUpdateInputSerializer",
]
_RIDES_EXPORTS = [
"RideParkOutputSerializer",
"RideModelOutputSerializer",
"RideListOutputSerializer",
"RideDetailOutputSerializer",
"RideCreateInputSerializer",
"RideUpdateInputSerializer",
"RideFilterInputSerializer",
"RollerCoasterStatsOutputSerializer",
"RollerCoasterStatsCreateInputSerializer",
"RollerCoasterStatsUpdateInputSerializer",
"RideLocationOutputSerializer",
"RideLocationCreateInputSerializer",
"RideLocationUpdateInputSerializer",
"RideReviewOutputSerializer",
"RideReviewCreateInputSerializer",
"RideReviewUpdateInputSerializer",
]
_SERVICES_EXPORTS = [
"HealthCheckOutputSerializer",
"PerformanceMetricsOutputSerializer",
"SimpleHealthOutputSerializer",
"EmailSendInputSerializer",
"EmailTemplateOutputSerializer",
"MapDataOutputSerializer",
"CoordinateInputSerializer",
"HistoryEventSerializer",
"HistoryEntryOutputSerializer",
"HistoryCreateInputSerializer",
"ModerationSubmissionSerializer",
"ModerationSubmissionOutputSerializer",
"RoadtripParkSerializer",
"RoadtripCreateInputSerializer",
"RoadtripOutputSerializer",
"GeocodeInputSerializer",
"GeocodeOutputSerializer",
"DistanceCalculationInputSerializer",
"DistanceCalculationOutputSerializer",
]
# Build a static __all__ list with only the serializers we know exist
__all__ = [
# Shared exports
"FilterOptionSerializer",
"FilterRangeSerializer",
"StandardizedFilterMetadataSerializer",
"validate_filter_metadata_contract",
"ensure_filter_option_format",
# Parks exports
"ParkListOutputSerializer",
"ParkDetailOutputSerializer",
"ParkCreateInputSerializer",
"ParkUpdateInputSerializer",
"ParkFilterInputSerializer",
"ParkAreaDetailOutputSerializer",
"ParkAreaCreateInputSerializer",
"ParkAreaUpdateInputSerializer",
"ParkLocationOutputSerializer",
"ParkLocationCreateInputSerializer",
"ParkLocationUpdateInputSerializer",
"ParkSuggestionSerializer",
"ParkSuggestionOutputSerializer",
# Companies exports
"CompanyDetailOutputSerializer",
"CompanyCreateInputSerializer",
"CompanyUpdateInputSerializer",
"RideModelDetailOutputSerializer",
"RideModelCreateInputSerializer",
"RideModelUpdateInputSerializer",
# Rides exports
"RideParkOutputSerializer",
"RideModelOutputSerializer",
"RideListOutputSerializer",
"RideDetailOutputSerializer",
"RideCreateInputSerializer",
"RideUpdateInputSerializer",
"RideFilterInputSerializer",
"RollerCoasterStatsOutputSerializer",
"RollerCoasterStatsCreateInputSerializer",
"RollerCoasterStatsUpdateInputSerializer",
"RideLocationOutputSerializer",
"RideLocationCreateInputSerializer",
"RideLocationUpdateInputSerializer",
"RideReviewOutputSerializer",
"RideReviewCreateInputSerializer",
"RideReviewUpdateInputSerializer",
# Services exports
"HealthCheckOutputSerializer",
"PerformanceMetricsOutputSerializer",
"SimpleHealthOutputSerializer",
"EmailSendInputSerializer",
"EmailTemplateOutputSerializer",
"MapDataOutputSerializer",
"CoordinateInputSerializer",
"HistoryEventSerializer",
"HistoryEntryOutputSerializer",
"HistoryCreateInputSerializer",
"ModerationSubmissionSerializer",
"ModerationSubmissionOutputSerializer",
"RoadtripParkSerializer",
"RoadtripCreateInputSerializer",
"RoadtripOutputSerializer",
"GeocodeInputSerializer",
"GeocodeOutputSerializer",
"DistanceCalculationInputSerializer",
"DistanceCalculationOutputSerializer",
]
# Add any accounts serializers that actually exist
for name in _ACCOUNTS_SYMBOLS:
if name in globals():
__all__.append(name)

View File

@@ -1,904 +0,0 @@
"""
User accounts and settings serializers for ThrillWiki API v1.
This module contains all serializers related to user account management,
profile settings, preferences, privacy, notifications, and security.
"""
from rest_framework import serializers
from django.contrib.auth import get_user_model
from drf_spectacular.utils import (
extend_schema_serializer,
OpenApiExample,
)
from apps.accounts.models import (
User,
UserProfile,
TopList,
UserNotification,
NotificationPreference,
)
from apps.core.choices.serializers import RichChoiceFieldSerializer
UserModel = get_user_model()
# === USER PROFILE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Profile Example",
summary="Complete user profile",
description="Full user profile with all fields",
value={
"user_id": "1234",
"username": "thrillseeker",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"is_active": True,
"date_joined": "2024-01-01T00:00:00Z",
"role": "USER",
"theme_preference": "dark",
"profile": {
"profile_id": "5678",
"display_name": "Thrill Seeker",
"avatar": "https://example.com/avatars/user.jpg",
"pronouns": "they/them",
"bio": "Love roller coasters and theme parks!",
"twitter": "https://twitter.com/thrillseeker",
"instagram": "https://instagram.com/thrillseeker",
"youtube": "https://youtube.com/thrillseeker",
"discord": "thrillseeker#1234",
"coaster_credits": 150,
"dark_ride_credits": 45,
"flat_ride_credits": 89,
"water_ride_credits": 23,
},
},
)
]
)
class UserProfileSerializer(serializers.ModelSerializer):
"""Serializer for user profile data."""
avatar_url = serializers.SerializerMethodField()
avatar_variants = serializers.SerializerMethodField()
class Meta:
model = UserProfile
fields = [
"profile_id",
"display_name",
"avatar",
"avatar_url",
"avatar_variants",
"pronouns",
"bio",
"twitter",
"instagram",
"youtube",
"discord",
"coaster_credits",
"dark_ride_credits",
"flat_ride_credits",
"water_ride_credits",
]
read_only_fields = ["profile_id", "avatar_url", "avatar_variants"]
def get_avatar_url(self, obj):
"""Get the avatar URL with fallback to default letter-based avatar."""
return obj.get_avatar_url()
def get_avatar_variants(self, obj):
"""Get avatar variants for different use cases."""
return obj.get_avatar_variants()
def validate_display_name(self, value):
"""Validate display name uniqueness - now checks User model first."""
user = self.context["request"].user
# Check User model for display_name uniqueness (primary location)
if User.objects.filter(display_name=value).exclude(id=user.id).exists():
raise serializers.ValidationError("Display name already taken")
# Also check UserProfile for backward compatibility during transition
if UserProfile.objects.filter(display_name=value).exclude(user=user).exists():
raise serializers.ValidationError("Display name already taken")
return value
@extend_schema_serializer(
examples=[
OpenApiExample(
"Complete User Example",
summary="Complete user with profile",
description="Full user object with embedded profile",
value={
"user_id": "1234",
"username": "thrillseeker",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"is_active": True,
"date_joined": "2024-01-01T00:00:00Z",
"role": "USER",
"theme_preference": "dark",
"profile": {
"profile_id": "5678",
"display_name": "Thrill Seeker",
"avatar": "https://example.com/avatars/user.jpg",
"pronouns": "they/them",
"bio": "Love roller coasters and theme parks!",
"twitter": "https://twitter.com/thrillseeker",
"instagram": "https://instagram.com/thrillseeker",
"youtube": "https://youtube.com/thrillseeker",
"discord": "thrillseeker#1234",
"coaster_credits": 150,
"dark_ride_credits": 45,
"flat_ride_credits": 89,
"water_ride_credits": 23,
},
},
)
]
)
class CompleteUserSerializer(serializers.ModelSerializer):
"""Complete user serializer with profile data."""
profile = UserProfileSerializer(read_only=True)
class Meta:
model = User
fields = [
"user_id",
"username",
"email",
"first_name",
"last_name",
"is_active",
"date_joined",
"role",
"theme_preference",
"profile",
]
read_only_fields = ["user_id", "date_joined", "role"]
# === USER SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Preferences Example",
summary="User preferences and settings",
description="User's preference settings",
value={
"theme_preference": "dark",
"email_notifications": True,
"push_notifications": False,
"privacy_level": "public",
"show_email": False,
"show_real_name": True,
"show_statistics": True,
"allow_friend_requests": True,
"allow_messages": True,
},
)
]
)
class UserPreferencesSerializer(serializers.Serializer):
"""Serializer for user preferences and settings."""
theme_preference = RichChoiceFieldSerializer(
choice_group="theme_preferences",
domain="accounts",
help_text="User's theme preference"
)
email_notifications = serializers.BooleanField(
default=True, help_text="Whether to receive email notifications"
)
push_notifications = serializers.BooleanField(
default=False, help_text="Whether to receive push notifications"
)
privacy_level = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
default="public",
help_text="Profile visibility level",
)
show_email = serializers.BooleanField(
default=False, help_text="Whether to show email on profile"
)
show_real_name = serializers.BooleanField(
default=True, help_text="Whether to show real name on profile"
)
show_statistics = serializers.BooleanField(
default=True, help_text="Whether to show ride statistics on profile"
)
allow_friend_requests = serializers.BooleanField(
default=True, help_text="Whether to allow friend requests"
)
allow_messages = serializers.BooleanField(
default=True, help_text="Whether to allow direct messages"
)
# === NOTIFICATION SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Notification Settings Example",
summary="User notification preferences",
description="Detailed notification settings",
value={
"email_notifications": {
"new_reviews": True,
"review_replies": True,
"friend_requests": True,
"messages": True,
"weekly_digest": False,
"new_features": True,
"security_alerts": True,
},
"push_notifications": {
"new_reviews": False,
"review_replies": True,
"friend_requests": True,
"messages": True,
},
"in_app_notifications": {
"new_reviews": True,
"review_replies": True,
"friend_requests": True,
"messages": True,
"system_announcements": True,
},
},
)
]
)
class NotificationSettingsSerializer(serializers.Serializer):
"""Serializer for detailed notification settings."""
class EmailNotificationsSerializer(serializers.Serializer):
new_reviews = serializers.BooleanField(default=True)
review_replies = serializers.BooleanField(default=True)
friend_requests = serializers.BooleanField(default=True)
messages = serializers.BooleanField(default=True)
weekly_digest = serializers.BooleanField(default=False)
new_features = serializers.BooleanField(default=True)
security_alerts = serializers.BooleanField(default=True)
class PushNotificationsSerializer(serializers.Serializer):
new_reviews = serializers.BooleanField(default=False)
review_replies = serializers.BooleanField(default=True)
friend_requests = serializers.BooleanField(default=True)
messages = serializers.BooleanField(default=True)
class InAppNotificationsSerializer(serializers.Serializer):
new_reviews = serializers.BooleanField(default=True)
review_replies = serializers.BooleanField(default=True)
friend_requests = serializers.BooleanField(default=True)
messages = serializers.BooleanField(default=True)
system_announcements = serializers.BooleanField(default=True)
email_notifications = EmailNotificationsSerializer()
push_notifications = PushNotificationsSerializer()
in_app_notifications = InAppNotificationsSerializer()
# === PRIVACY SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Privacy Settings Example",
summary="User privacy settings",
description="Detailed privacy and visibility settings",
value={
"profile_visibility": "public",
"show_email": False,
"show_real_name": True,
"show_join_date": True,
"show_statistics": True,
"show_reviews": True,
"show_photos": True,
"show_top_lists": True,
"allow_friend_requests": True,
"allow_messages": True,
"allow_profile_comments": False,
"search_visibility": True,
"activity_visibility": "friends",
},
)
]
)
class PrivacySettingsSerializer(serializers.Serializer):
"""Serializer for privacy and visibility settings."""
profile_visibility = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
default="public",
help_text="Overall profile visibility",
)
show_email = serializers.BooleanField(
default=False, help_text="Show email address on profile"
)
show_real_name = serializers.BooleanField(
default=True, help_text="Show real name on profile"
)
show_join_date = serializers.BooleanField(
default=True, help_text="Show join date on profile"
)
show_statistics = serializers.BooleanField(
default=True, help_text="Show ride statistics on profile"
)
show_reviews = serializers.BooleanField(
default=True, help_text="Show reviews on profile"
)
show_photos = serializers.BooleanField(
default=True, help_text="Show uploaded photos on profile"
)
show_top_lists = serializers.BooleanField(
default=True, help_text="Show top lists on profile"
)
allow_friend_requests = serializers.BooleanField(
default=True, help_text="Allow others to send friend requests"
)
allow_messages = serializers.BooleanField(
default=True, help_text="Allow others to send direct messages"
)
allow_profile_comments = serializers.BooleanField(
default=False, help_text="Allow others to comment on profile"
)
search_visibility = serializers.BooleanField(
default=True, help_text="Allow profile to appear in search results"
)
activity_visibility = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
default="friends",
help_text="Who can see your activity feed",
)
# === SECURITY SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Security Settings Example",
summary="User security settings",
description="Account security and authentication settings",
value={
"two_factor_enabled": False,
"login_notifications": True,
"session_timeout": 30,
"require_password_change": False,
"last_password_change": "2024-01-01T00:00:00Z",
"active_sessions": 2,
"login_history_retention": 90,
},
)
]
)
class SecuritySettingsSerializer(serializers.Serializer):
"""Serializer for security settings."""
two_factor_enabled = serializers.BooleanField(
default=False, help_text="Whether two-factor authentication is enabled"
)
login_notifications = serializers.BooleanField(
default=True, help_text="Send notifications for new logins"
)
session_timeout = serializers.IntegerField(
default=30, min_value=5, max_value=180, help_text="Session timeout in days"
)
require_password_change = serializers.BooleanField(
default=False, help_text="Whether password change is required"
)
last_password_change = serializers.DateTimeField(
read_only=True, help_text="When password was last changed"
)
active_sessions = serializers.IntegerField(
read_only=True, help_text="Number of active sessions"
)
login_history_retention = serializers.IntegerField(
default=90,
min_value=30,
max_value=365,
help_text="How long to keep login history (days)",
)
# === USER STATISTICS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Statistics Example",
summary="User activity statistics",
description="Comprehensive user activity and contribution statistics",
value={
"ride_credits": {
"coaster_credits": 150,
"dark_ride_credits": 45,
"flat_ride_credits": 89,
"water_ride_credits": 23,
"total_credits": 307,
},
"contributions": {
"park_reviews": 25,
"ride_reviews": 87,
"photos_uploaded": 156,
"top_lists_created": 8,
"helpful_votes_received": 342,
},
"activity": {
"days_active": 45,
"last_active": "2024-01-15T10:30:00Z",
"average_review_rating": 4.2,
"most_reviewed_park": "Cedar Point",
"favorite_ride_type": "Roller Coaster",
},
"achievements": {
"first_review": True,
"photo_contributor": True,
"top_reviewer": False,
"park_explorer": True,
"coaster_enthusiast": True,
},
},
)
]
)
class UserStatisticsSerializer(serializers.Serializer):
"""Serializer for user statistics and achievements."""
class RideCreditsSerializer(serializers.Serializer):
coaster_credits = serializers.IntegerField()
dark_ride_credits = serializers.IntegerField()
flat_ride_credits = serializers.IntegerField()
water_ride_credits = serializers.IntegerField()
total_credits = serializers.IntegerField()
class ContributionsSerializer(serializers.Serializer):
park_reviews = serializers.IntegerField()
ride_reviews = serializers.IntegerField()
photos_uploaded = serializers.IntegerField()
top_lists_created = serializers.IntegerField()
helpful_votes_received = serializers.IntegerField()
class ActivitySerializer(serializers.Serializer):
days_active = serializers.IntegerField()
last_active = serializers.DateTimeField()
average_review_rating = serializers.FloatField()
most_reviewed_park = serializers.CharField()
favorite_ride_type = serializers.CharField()
class AchievementsSerializer(serializers.Serializer):
first_review = serializers.BooleanField()
photo_contributor = serializers.BooleanField()
top_reviewer = serializers.BooleanField()
park_explorer = serializers.BooleanField()
coaster_enthusiast = serializers.BooleanField()
ride_credits = RideCreditsSerializer()
contributions = ContributionsSerializer()
activity = ActivitySerializer()
achievements = AchievementsSerializer()
# === TOP LISTS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Top List Example",
summary="User's top list",
description="A user's ranked list of rides or parks",
value={
"id": 1,
"title": "My Top 10 Roller Coasters",
"category": "RC",
"description": "My favorite roller coasters from around the world",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"items_count": 10,
},
)
]
)
class TopListSerializer(serializers.ModelSerializer):
"""Serializer for user's top lists."""
items_count = serializers.SerializerMethodField()
class Meta:
model = TopList
fields = [
"id",
"title",
"category",
"description",
"created_at",
"updated_at",
"items_count",
]
read_only_fields = ["id", "created_at", "updated_at"]
def get_items_count(self, obj):
"""Get the number of items in the list."""
return obj.items.count()
# === ACCOUNT UPDATE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Account Update Example",
summary="Update account information",
description="Update basic account information",
value={
"first_name": "John",
"last_name": "Doe",
"email": "newemail@example.com",
},
)
]
)
class AccountUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating account information."""
class Meta:
model = User
fields = [
"first_name",
"last_name",
"email",
]
def validate_email(self, value):
"""Validate email uniqueness."""
user = self.context["request"].user
if User.objects.filter(email=value).exclude(id=user.id).exists():
raise serializers.ValidationError("Email already in use")
return value
# === PROFILE UPDATE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Profile Update Example",
summary="Update profile information",
description="Update profile information and social links",
value={
"display_name": "New Display Name",
"pronouns": "they/them",
"bio": "Updated bio text",
"twitter": "https://twitter.com/newhandle",
"instagram": "",
"youtube": "https://youtube.com/newchannel",
"discord": "newhandle#5678",
},
)
]
)
class ProfileUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating profile information."""
class Meta:
model = UserProfile
fields = [
"display_name",
"pronouns",
"bio",
"twitter",
"instagram",
"youtube",
"discord",
]
def validate_display_name(self, value):
"""Validate display name uniqueness - now checks User model first."""
user = self.context["request"].user
# Check User model for display_name uniqueness (primary location)
if User.objects.filter(display_name=value).exclude(id=user.id).exists():
raise serializers.ValidationError("Display name already taken")
# Also check UserProfile for backward compatibility during transition
if UserProfile.objects.filter(display_name=value).exclude(user=user).exists():
raise serializers.ValidationError("Display name already taken")
return value
# === THEME PREFERENCE SERIALIZER ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Theme Update Example",
summary="Update theme preference",
description="Update user's theme preference",
value={
"theme_preference": "dark",
},
)
]
)
class ThemePreferenceSerializer(serializers.ModelSerializer):
"""Serializer for updating theme preference."""
class Meta:
model = User
fields = ["theme_preference"]
# === NOTIFICATION SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Notification Example",
summary="User notification",
description="A notification sent to a user",
value={
"id": 1,
"notification_type": "submission_approved",
"title": "Your submission has been approved!",
"message": "Your photo submission for Cedar Point has been approved and is now live on the site.",
"priority": "normal",
"is_read": False,
"read_at": None,
"created_at": "2024-01-15T10:30:00Z",
"expires_at": None,
"extra_data": {"submission_id": 123, "park_name": "Cedar Point"},
},
)
]
)
class UserNotificationSerializer(serializers.ModelSerializer):
"""Serializer for user notifications."""
class Meta:
model = UserNotification
fields = [
"id",
"notification_type",
"title",
"message",
"priority",
"is_read",
"read_at",
"created_at",
"expires_at",
"extra_data",
]
read_only_fields = [
"id",
"notification_type",
"title",
"message",
"priority",
"created_at",
"expires_at",
"extra_data",
]
@extend_schema_serializer(
examples=[
OpenApiExample(
"Notification Preferences Example",
summary="User notification preferences",
description="Comprehensive notification preferences for all channels",
value={
"submission_approved_email": True,
"submission_approved_push": True,
"submission_approved_inapp": True,
"submission_rejected_email": True,
"submission_rejected_push": True,
"submission_rejected_inapp": True,
"submission_pending_email": False,
"submission_pending_push": False,
"submission_pending_inapp": True,
"review_reply_email": True,
"review_reply_push": True,
"review_reply_inapp": True,
"review_helpful_email": False,
"review_helpful_push": True,
"review_helpful_inapp": True,
"friend_request_email": True,
"friend_request_push": True,
"friend_request_inapp": True,
"friend_accepted_email": False,
"friend_accepted_push": True,
"friend_accepted_inapp": True,
"message_received_email": True,
"message_received_push": True,
"message_received_inapp": True,
"system_announcement_email": True,
"system_announcement_push": False,
"system_announcement_inapp": True,
"account_security_email": True,
"account_security_push": True,
"account_security_inapp": True,
"feature_update_email": True,
"feature_update_push": False,
"feature_update_inapp": True,
"achievement_unlocked_email": False,
"achievement_unlocked_push": True,
"achievement_unlocked_inapp": True,
"milestone_reached_email": False,
"milestone_reached_push": True,
"milestone_reached_inapp": True,
},
)
]
)
class NotificationPreferenceSerializer(serializers.ModelSerializer):
"""Serializer for notification preferences."""
class Meta:
model = NotificationPreference
fields = [
# Submission notifications
"submission_approved_email",
"submission_approved_push",
"submission_approved_inapp",
"submission_rejected_email",
"submission_rejected_push",
"submission_rejected_inapp",
"submission_pending_email",
"submission_pending_push",
"submission_pending_inapp",
# Review notifications
"review_reply_email",
"review_reply_push",
"review_reply_inapp",
"review_helpful_email",
"review_helpful_push",
"review_helpful_inapp",
# Social notifications
"friend_request_email",
"friend_request_push",
"friend_request_inapp",
"friend_accepted_email",
"friend_accepted_push",
"friend_accepted_inapp",
"message_received_email",
"message_received_push",
"message_received_inapp",
# System notifications
"system_announcement_email",
"system_announcement_push",
"system_announcement_inapp",
"account_security_email",
"account_security_push",
"account_security_inapp",
"feature_update_email",
"feature_update_push",
"feature_update_inapp",
# Achievement notifications
"achievement_unlocked_email",
"achievement_unlocked_push",
"achievement_unlocked_inapp",
"milestone_reached_email",
"milestone_reached_push",
"milestone_reached_inapp",
]
# === NOTIFICATION ACTIONS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Mark Notifications Read Example",
summary="Mark notifications as read",
description="Mark specific notifications as read",
value={"notification_ids": [1, 2, 3, 4, 5]},
)
]
)
class MarkNotificationsReadSerializer(serializers.Serializer):
"""Serializer for marking notifications as read."""
notification_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of notification IDs to mark as read",
)
def validate_notification_ids(self, value):
"""Validate that all notification IDs belong to the requesting user."""
user = self.context["request"].user
valid_ids = UserNotification.objects.filter(
id__in=value, user=user
).values_list("id", flat=True)
invalid_ids = set(value) - set(valid_ids)
if invalid_ids:
raise serializers.ValidationError(
f"Invalid notification IDs: {list(invalid_ids)}"
)
return value
@extend_schema_serializer(
examples=[
OpenApiExample(
"Avatar Upload Example",
summary="Upload user avatar",
description="Upload a new avatar image",
value={"avatar": "base64_encoded_image_data_or_file_upload"},
)
]
)
class AvatarUploadSerializer(serializers.Serializer):
"""Serializer for uploading user avatar."""
# Use FileField instead of ImageField to bypass Django's image validation
avatar = serializers.FileField()
def validate_avatar(self, value):
"""Validate avatar file."""
if not value:
raise serializers.ValidationError("No file provided")
# Check file size constraints (max 10MB for Cloudflare Images)
if hasattr(value, 'size') and value.size > 10 * 1024 * 1024:
raise serializers.ValidationError(
"Image file too large. Maximum size is 10MB.")
# Try to validate with PIL
try:
from PIL import Image
import io
value.seek(0)
image_data = value.read()
value.seek(0) # Reset for later use
if len(image_data) == 0:
raise serializers.ValidationError("File appears to be empty")
# Try to open with PIL
image = Image.open(io.BytesIO(image_data))
# Verify it's a valid image
image.verify()
# Check image dimensions (max 12,000x12,000 for Cloudflare Images)
if image.size[0] > 12000 or image.size[1] > 12000:
raise serializers.ValidationError(
"Image dimensions too large. Maximum is 12,000x12,000 pixels.")
# Check if it's a supported format
if image.format not in ['JPEG', 'PNG', 'GIF', 'WEBP']:
raise serializers.ValidationError(
f"Unsupported image format: {image.format}. Supported formats: JPEG, PNG, GIF, WebP.")
except serializers.ValidationError:
raise # Re-raise validation errors
except Exception:
# PIL validation failed, but let Cloudflare Images try to process it
pass
return value

View File

@@ -1,497 +0,0 @@
"""
Authentication domain serializers for ThrillWiki API v1.
This module contains all serializers related to user authentication,
registration, password management, and social authentication.
"""
from rest_framework import serializers
from django.contrib.auth import get_user_model, authenticate
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from drf_spectacular.utils import (
extend_schema_serializer,
OpenApiExample,
)
UserModel = get_user_model()
# === USER SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Output Example",
summary="Example user response",
description="A typical user object in API responses",
value={
"id": 1,
"username": "thrillseeker",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"is_active": True,
"date_joined": "2024-01-01T00:00:00Z",
},
)
]
)
class UserOutputSerializer(serializers.ModelSerializer):
"""Output serializer for user data."""
class Meta:
model = UserModel
fields = [
"id",
"username",
"email",
"first_name",
"last_name",
"is_active",
"date_joined",
]
read_only_fields = ["id", "date_joined"]
# === LOGIN SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Login Input Example",
summary="Example login request",
description="Login with username or email and password",
value={
"username": "thrillseeker",
"password": "securepassword123",
},
)
]
)
class LoginInputSerializer(serializers.Serializer):
"""Input serializer for user login."""
username = serializers.CharField(
max_length=150,
help_text="Username or email address",
)
password = serializers.CharField(
write_only=True,
style={"input_type": "password"},
help_text="User password",
)
def validate(self, attrs):
"""Validate login credentials."""
username = attrs.get("username")
password = attrs.get("password")
if username and password:
# Try to authenticate with the provided credentials
user = authenticate(
request=self.context.get("request"),
username=username,
password=password,
)
if not user:
# Try email-based authentication if username failed
if "@" in username:
try:
user_obj = UserModel.objects.get(email=username)
user = authenticate(
request=self.context.get("request"),
username=user_obj.username, # type: ignore[attr-defined]
password=password,
)
except UserModel.DoesNotExist:
pass
if not user:
raise serializers.ValidationError("Invalid credentials")
if not user.is_active:
raise serializers.ValidationError("Account is disabled")
attrs["user"] = user
else:
raise serializers.ValidationError("Must include username and password")
return attrs
@extend_schema_serializer(
examples=[
OpenApiExample(
"Login Output Example",
summary="Example login response",
description="Successful login response with token and user data",
value={
"token": "abc123def456ghi789",
"user": {
"id": 1,
"username": "thrillseeker",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
},
"message": "Login successful",
},
)
]
)
class LoginOutputSerializer(serializers.Serializer):
"""Output serializer for login response."""
token = serializers.CharField(help_text="Authentication token")
user = UserOutputSerializer(help_text="User information")
message = serializers.CharField(help_text="Success message")
# === SIGNUP SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Signup Input Example",
summary="Example registration request",
description="Register a new user account",
value={
"username": "newuser",
"email": "newuser@example.com",
"password": "securepassword123",
"password_confirm": "securepassword123",
"first_name": "Jane",
"last_name": "Smith",
},
)
]
)
class SignupInputSerializer(serializers.ModelSerializer):
"""Input serializer for user registration."""
password = serializers.CharField(
write_only=True,
style={"input_type": "password"},
help_text="User password",
)
password_confirm = serializers.CharField(
write_only=True,
style={"input_type": "password"},
help_text="Password confirmation",
)
class Meta:
model = UserModel
fields = [
"username",
"email",
"password",
"password_confirm",
"first_name",
"last_name",
]
def validate_email(self, value):
"""Validate email uniqueness."""
if UserModel.objects.filter(email=value).exists():
raise serializers.ValidationError("Email already registered")
return value
def validate_username(self, value):
"""Validate username uniqueness."""
if UserModel.objects.filter(username=value).exists():
raise serializers.ValidationError("Username already taken")
return value
def validate_password(self, value):
"""Validate password strength."""
try:
validate_password(value)
except DjangoValidationError as e:
raise serializers.ValidationError(list(e.messages))
return value
def validate(self, attrs):
"""Cross-field validation."""
password = attrs.get("password")
password_confirm = attrs.get("password_confirm")
if password != password_confirm:
raise serializers.ValidationError("Passwords do not match")
return attrs
def create(self, validated_data):
"""Create new user."""
validated_data.pop("password_confirm")
password = validated_data.pop("password")
user = UserModel.objects.create_user( # type: ignore[attr-defined]
password=password,
**validated_data,
)
return user
@extend_schema_serializer(
examples=[
OpenApiExample(
"Signup Output Example",
summary="Example registration response",
description="Successful registration response with token and user data",
value={
"token": "abc123def456ghi789",
"user": {
"id": 2,
"username": "newuser",
"email": "newuser@example.com",
"first_name": "Jane",
"last_name": "Smith",
},
"message": "Registration successful",
},
)
]
)
class SignupOutputSerializer(serializers.Serializer):
"""Output serializer for registration response."""
token = serializers.CharField(help_text="Authentication token")
user = UserOutputSerializer(help_text="User information")
message = serializers.CharField(help_text="Success message")
# === LOGOUT SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Logout Output Example",
summary="Example logout response",
description="Successful logout response",
value={
"message": "Logout successful",
},
)
]
)
class LogoutOutputSerializer(serializers.Serializer):
"""Output serializer for logout response."""
message = serializers.CharField(help_text="Success message")
# === PASSWORD RESET SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Password Reset Input Example",
summary="Example password reset request",
description="Request password reset email",
value={
"email": "user@example.com",
},
)
]
)
class PasswordResetInputSerializer(serializers.Serializer):
"""Input serializer for password reset request."""
email = serializers.EmailField(help_text="Email address for password reset")
def validate_email(self, value):
"""Validate email exists."""
if not UserModel.objects.filter(email=value).exists():
# Don't reveal if email exists for security
pass
return value
def save(self, **kwargs): # type: ignore[override]
"""Send password reset email."""
email = self.validated_data["email"] # type: ignore[index]
try:
_user = UserModel.objects.get(email=email)
# Here you would typically send a password reset email
# For now, we'll just pass
pass
except UserModel.DoesNotExist:
# Don't reveal if email exists for security
pass
@extend_schema_serializer(
examples=[
OpenApiExample(
"Password Reset Output Example",
summary="Example password reset response",
description="Password reset email sent response",
value={
"detail": "Password reset email sent",
},
)
]
)
class PasswordResetOutputSerializer(serializers.Serializer):
"""Output serializer for password reset response."""
detail = serializers.CharField(help_text="Success message")
# === PASSWORD CHANGE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Password Change Input Example",
summary="Example password change request",
description="Change current user's password",
value={
"old_password": "oldpassword123",
"new_password": "newpassword456",
"new_password_confirm": "newpassword456",
},
)
]
)
class PasswordChangeInputSerializer(serializers.Serializer):
"""Input serializer for password change."""
old_password = serializers.CharField(
write_only=True,
style={"input_type": "password"},
help_text="Current password",
)
new_password = serializers.CharField(
write_only=True,
style={"input_type": "password"},
help_text="New password",
)
new_password_confirm = serializers.CharField(
write_only=True,
style={"input_type": "password"},
help_text="New password confirmation",
)
def validate_old_password(self, value):
"""Validate current password."""
user = self.context["request"].user
if not user.check_password(value):
raise serializers.ValidationError("Current password is incorrect")
return value
def validate_new_password(self, value):
"""Validate new password strength."""
try:
validate_password(value, user=self.context["request"].user)
except DjangoValidationError as e:
raise serializers.ValidationError(list(e.messages))
return value
def validate(self, attrs):
"""Cross-field validation."""
new_password = attrs.get("new_password")
new_password_confirm = attrs.get("new_password_confirm")
if new_password != new_password_confirm:
raise serializers.ValidationError("New passwords do not match")
return attrs
def save(self, **kwargs): # type: ignore[override]
"""Change user password."""
user = self.context["request"].user
user.set_password(self.validated_data["new_password"]) # type: ignore[index]
user.save()
return user
@extend_schema_serializer(
examples=[
OpenApiExample(
"Password Change Output Example",
summary="Example password change response",
description="Password changed successfully response",
value={
"detail": "Password changed successfully",
},
)
]
)
class PasswordChangeOutputSerializer(serializers.Serializer):
"""Output serializer for password change response."""
detail = serializers.CharField(help_text="Success message")
# === SOCIAL PROVIDER SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Social Provider Example",
summary="Example social provider",
description="Available social authentication provider",
value={
"id": "google",
"name": "Google",
"authUrl": "https://example.com/accounts/google/login/",
},
)
]
)
class SocialProviderOutputSerializer(serializers.Serializer):
"""Output serializer for social authentication providers."""
id = serializers.CharField(help_text="Provider ID")
name = serializers.CharField(help_text="Provider display name")
authUrl = serializers.URLField(help_text="Authentication URL")
# === AUTH STATUS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Auth Status Authenticated Example",
summary="Example authenticated status",
description="Response when user is authenticated",
value={
"authenticated": True,
"user": {
"id": 1,
"username": "thrillseeker",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
},
},
),
OpenApiExample(
"Auth Status Unauthenticated Example",
summary="Example unauthenticated status",
description="Response when user is not authenticated",
value={
"authenticated": False,
"user": None,
},
),
]
)
class AuthStatusOutputSerializer(serializers.Serializer):
"""Output serializer for authentication status."""
authenticated = serializers.BooleanField(help_text="Whether user is authenticated")
user = UserOutputSerializer(
allow_null=True, help_text="User information if authenticated"
)

View File

@@ -1,153 +0,0 @@
"""
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 ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === 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 = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
# 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=ModelChoices.get_ride_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=ModelChoices.get_ride_category_choices(), required=False)
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)

View File

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

@@ -1,421 +0,0 @@
"""
Maps domain serializers for ThrillWiki API v1.
This module contains all serializers related to map functionality,
including location data, search results, and clustering.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_serializer,
extend_schema_field,
OpenApiExample,
)
# === MAP LOCATION SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Map Location Example",
summary="Example map location response",
description="A location point on the map",
value={
"id": 1,
"type": "park",
"name": "Cedar Point",
"slug": "cedar-point",
"latitude": 41.4793,
"longitude": -82.6833,
"status": "OPERATING",
"location": {
"city": "Sandusky",
"state": "Ohio",
"country": "United States",
},
"stats": {
"coaster_count": 17,
"ride_count": 70,
"average_rating": 4.5,
},
},
)
]
)
class MapLocationSerializer(serializers.Serializer):
"""Serializer for individual map locations (parks and rides)."""
id = serializers.IntegerField()
type = serializers.CharField() # 'park' or 'ride'
name = serializers.CharField()
slug = serializers.CharField()
latitude = serializers.FloatField(allow_null=True)
longitude = serializers.FloatField(allow_null=True)
status = serializers.CharField()
# Location details
location = serializers.SerializerMethodField()
# Statistics
stats = serializers.SerializerMethodField()
@extend_schema_field(serializers.DictField())
def get_location(self, obj) -> dict:
"""Get location information."""
if hasattr(obj, "location") and obj.location:
return {
"city": obj.location.city,
"state": obj.location.state,
"country": obj.location.country,
"formatted_address": obj.location.formatted_address,
}
return {}
@extend_schema_field(serializers.DictField())
def get_stats(self, obj) -> dict:
"""Get relevant statistics based on object type."""
if obj._meta.model_name == "park":
return {
"coaster_count": obj.coaster_count or 0,
"ride_count": obj.ride_count or 0,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
}
elif obj._meta.model_name == "ride":
return {
"category": obj.get_category_display() if obj.category else None,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"park_name": obj.park.name if obj.park else None,
}
return {}
@extend_schema_serializer(
examples=[
OpenApiExample(
"Map Cluster Example",
summary="Example map cluster response",
description="A cluster of locations on the map",
value={
"id": "cluster_1",
"type": "cluster",
"latitude": 41.5,
"longitude": -82.7,
"count": 5,
"bounds": {
"north": 41.6,
"south": 41.4,
"east": -82.6,
"west": -82.8,
},
},
)
]
)
class MapClusterSerializer(serializers.Serializer):
"""Serializer for map clusters."""
id = serializers.CharField()
type = serializers.CharField(default="cluster")
latitude = serializers.FloatField()
longitude = serializers.FloatField()
count = serializers.IntegerField()
bounds = serializers.DictField()
@extend_schema_serializer(
examples=[
OpenApiExample(
"Map Locations Response Example",
summary="Example map locations response",
description="Response containing locations and optional clusters",
value={
"status": "success",
"data": {
"locations": [
{
"id": 1,
"type": "park",
"name": "Cedar Point",
"slug": "cedar-point",
"latitude": 41.4793,
"longitude": -82.6833,
"status": "OPERATING",
}
],
"clusters": [],
"bounds": {
"north": 41.5,
"south": 41.4,
"east": -82.6,
"west": -82.8,
},
"total_count": 1,
"clustered": False,
},
},
)
]
)
class MapLocationsResponseSerializer(serializers.Serializer):
"""Response serializer for map locations endpoint."""
status = serializers.CharField(default="success")
locations = serializers.ListField(child=serializers.DictField())
clusters = serializers.ListField(child=serializers.DictField(), default=list)
bounds = serializers.DictField(default=dict)
total_count = serializers.IntegerField(default=0)
clustered = serializers.BooleanField(default=False)
# === MAP SEARCH SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Map Search Result Example",
summary="Example map search result",
description="A search result for map locations",
value={
"id": 1,
"type": "park",
"name": "Cedar Point",
"slug": "cedar-point",
"latitude": 41.4793,
"longitude": -82.6833,
"location": {
"city": "Sandusky",
"state": "Ohio",
"country": "United States",
},
"relevance_score": 0.95,
},
)
]
)
class MapSearchResultSerializer(serializers.Serializer):
"""Serializer for map search results."""
id = serializers.IntegerField()
type = serializers.CharField()
name = serializers.CharField()
slug = serializers.CharField()
latitude = serializers.FloatField(allow_null=True)
longitude = serializers.FloatField(allow_null=True)
location = serializers.SerializerMethodField()
relevance_score = serializers.FloatField(required=False)
@extend_schema_field(serializers.DictField())
def get_location(self, obj) -> dict:
"""Get location information."""
if hasattr(obj, "location") and obj.location:
return {
"city": obj.location.city,
"state": obj.location.state,
"country": obj.location.country,
}
return {}
@extend_schema_serializer(
examples=[
OpenApiExample(
"Map Search Response Example",
summary="Example map search response",
description="Response containing search results",
value={
"status": "success",
"data": {
"results": [
{
"id": 1,
"type": "park",
"name": "Cedar Point",
"slug": "cedar-point",
"latitude": 41.4793,
"longitude": -82.6833,
}
],
"query": "cedar point",
"total_count": 1,
"page": 1,
"page_size": 20,
},
},
)
]
)
class MapSearchResponseSerializer(serializers.Serializer):
"""Response serializer for map search endpoint."""
status = serializers.CharField(default="success")
results = serializers.ListField(child=serializers.DictField())
query = serializers.CharField()
total_count = serializers.IntegerField(default=0)
page = serializers.IntegerField(default=1)
page_size = serializers.IntegerField(default=20)
# === MAP DETAIL SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Map Location Detail Example",
summary="Example map location detail response",
description="Detailed information about a specific location",
value={
"id": 1,
"type": "park",
"name": "Cedar Point",
"slug": "cedar-point",
"description": "America's Roller Coast",
"latitude": 41.4793,
"longitude": -82.6833,
"status": "OPERATING",
"location": {
"street_address": "1 Cedar Point Dr",
"city": "Sandusky",
"state": "Ohio",
"country": "United States",
"postal_code": "44870",
"formatted_address": "1 Cedar Point Dr, Sandusky, Ohio, 44870, United States",
},
"stats": {
"coaster_count": 17,
"ride_count": 70,
"average_rating": 4.5,
},
"nearby_locations": [],
},
)
]
)
class MapLocationDetailSerializer(serializers.Serializer):
"""Serializer for detailed map location information."""
id = serializers.IntegerField()
type = serializers.CharField()
name = serializers.CharField()
slug = serializers.CharField()
description = serializers.CharField()
latitude = serializers.FloatField(allow_null=True)
longitude = serializers.FloatField(allow_null=True)
status = serializers.CharField()
# Detailed location information
location = serializers.SerializerMethodField()
# Statistics
stats = serializers.SerializerMethodField()
# Nearby locations
nearby_locations = serializers.SerializerMethodField()
@extend_schema_field(serializers.DictField())
def get_location(self, obj) -> dict:
"""Get detailed location information."""
if hasattr(obj, "location") and obj.location:
return {
"street_address": obj.location.street_address,
"city": obj.location.city,
"state": obj.location.state,
"country": obj.location.country,
"postal_code": obj.location.postal_code,
"formatted_address": obj.location.formatted_address,
}
return {}
@extend_schema_field(serializers.DictField())
def get_stats(self, obj) -> dict:
"""Get detailed statistics based on object type."""
if obj._meta.model_name == "park":
return {
"coaster_count": obj.coaster_count or 0,
"ride_count": obj.ride_count or 0,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"size_acres": float(obj.size_acres) if obj.size_acres else None,
"opening_date": (
obj.opening_date.isoformat() if obj.opening_date else None
),
}
elif obj._meta.model_name == "ride":
return {
"category": obj.get_category_display() if obj.category else None,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"park_name": obj.park.name if obj.park else None,
"opening_date": (
obj.opening_date.isoformat() if obj.opening_date else None
),
"manufacturer": obj.manufacturer.name if obj.manufacturer else None,
}
return {}
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_nearby_locations(self, obj) -> list:
"""Get nearby locations (placeholder for now)."""
# TODO: Implement nearby location logic
return []
# === INPUT SERIALIZERS ===
class MapBoundsInputSerializer(serializers.Serializer):
"""Input serializer for map bounds queries."""
north = serializers.FloatField(min_value=-90, max_value=90)
south = serializers.FloatField(min_value=-90, max_value=90)
east = serializers.FloatField(min_value=-180, max_value=180)
west = serializers.FloatField(min_value=-180, max_value=180)
def validate(self, attrs):
"""Validate that bounds make geographic sense."""
if attrs["north"] <= attrs["south"]:
raise serializers.ValidationError(
"North bound must be greater than south bound"
)
# Handle longitude wraparound (e.g., crossing the international date line)
# For now, we'll require west < east for simplicity
if attrs["west"] >= attrs["east"]:
raise serializers.ValidationError("West bound must be less than east bound")
return attrs
class MapSearchInputSerializer(serializers.Serializer):
"""Input serializer for map search queries."""
q = serializers.CharField(min_length=1, max_length=255)
types = serializers.CharField(required=False, allow_blank=True)
bounds = MapBoundsInputSerializer(required=False)
page = serializers.IntegerField(min_value=1, default=1)
page_size = serializers.IntegerField(min_value=1, max_value=100, default=20)
def validate_types(self, value):
"""Validate location types."""
if not value:
return []
valid_types = ["park", "ride"]
types = [t.strip().lower() for t in value.split(",")]
for location_type in types:
if location_type not in valid_types:
raise serializers.ValidationError(
f"Invalid location type: {location_type}. Valid types: {', '.join(valid_types)}"
)
return types

View File

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

@@ -1,124 +0,0 @@
"""
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_field,
)
from .shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === 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 = RichChoiceFieldSerializer(
choice_group="health_statuses",
domain="core"
)
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 = RichChoiceFieldSerializer(
choice_group="simple_health_statuses",
domain="core"
)
timestamp = serializers.DateTimeField()
error = serializers.CharField(required=False)

View File

@@ -1,728 +0,0 @@
"""
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 config.django import base as settings
from .shared import LocationOutputSerializer, CompanyOutputSerializer, ModelChoices
from apps.core.services.media_url_service import MediaURLService
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === 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 = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
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()
# 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 park."""
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.slug}/"
@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"},
"photos": [
{
"id": 456,
"image_url": "https://imagedelivery.net/account-hash/def789ghi012/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/def789ghi012/thumbnail",
"medium": "https://imagedelivery.net/account-hash/def789ghi012/medium",
"large": "https://imagedelivery.net/account-hash/def789ghi012/large",
"public": "https://imagedelivery.net/account-hash/def789ghi012/public",
},
"caption": "Beautiful park entrance",
"is_primary": True,
}
],
"primary_photo": {
"id": 456,
"image_url": "https://imagedelivery.net/account-hash/def789ghi012/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/def789ghi012/thumbnail",
"medium": "https://imagedelivery.net/account-hash/def789ghi012/medium",
"large": "https://imagedelivery.net/account-hash/def789ghi012/large",
"public": "https://imagedelivery.net/account-hash/def789ghi012/public",
},
"caption": "Beautiful park entrance",
},
},
)
]
)
class ParkDetailOutputSerializer(serializers.Serializer):
"""Output serializer for park detail view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
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()
# Photos
photos = serializers.SerializerMethodField()
primary_photo = serializers.SerializerMethodField()
banner_image = serializers.SerializerMethodField()
card_image = serializers.SerializerMethodField()
# URL
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}/"
@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 []
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_photos(self, obj):
"""Get all approved photos for this park."""
from apps.parks.models import ParkPhoto
photos = ParkPhoto.objects.filter(park=obj, is_approved=True).order_by(
"-is_primary", "-created_at"
)[
:10
] # Limit to 10 photos
return [
{
"id": photo.pk,
"image_url": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
"image_variants": {
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "thumbnail"),
"medium": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "medium"),
"large": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "large"),
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "public"),
},
"caption": photo.caption,
"alt_text": photo.alt_text,
"is_primary": photo.is_primary,
}
for photo in photos
]
@extend_schema_field(serializers.DictField(allow_null=True))
def get_primary_photo(self, obj):
"""Get the primary photo for this park."""
from apps.parks.models import ParkPhoto
try:
photo = ParkPhoto.objects.filter(
park=obj, is_primary=True, is_approved=True
).first()
if photo and photo.image:
return {
"id": photo.pk,
"image_url": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
"image_variants": {
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "thumbnail"),
"medium": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "medium"),
"large": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "large"),
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "public"),
},
"caption": photo.caption,
"alt_text": photo.alt_text,
}
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 park 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.pk,
"image_url": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "public"),
"image_variants": {
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "thumbnail"),
"medium": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "medium"),
"large": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "large"),
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "public"),
},
"caption": obj.banner_image.caption,
"alt_text": obj.banner_image.alt_text,
}
# Fallback to latest approved photo
from apps.parks.models import ParkPhoto
try:
latest_photo = (
ParkPhoto.objects.filter(
park=obj, is_approved=True, image__isnull=False
)
.order_by("-created_at")
.first()
)
if latest_photo and latest_photo.image:
return {
"id": latest_photo.pk,
"image_url": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
"image_variants": {
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "thumbnail"),
"medium": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "medium"),
"large": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "large"),
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "public"),
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
"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 park 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.pk,
"image_url": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "public"),
"image_variants": {
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "thumbnail"),
"medium": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "medium"),
"large": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "large"),
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "public"),
},
"caption": obj.card_image.caption,
"alt_text": obj.card_image.alt_text,
}
# Fallback to latest approved photo
from apps.parks.models import ParkPhoto
try:
latest_photo = (
ParkPhoto.objects.filter(
park=obj, is_approved=True, image__isnull=False
)
.order_by("-created_at")
.first()
)
if latest_photo and latest_photo.image:
return {
"id": latest_photo.pk,
"image_url": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
"image_variants": {
"thumbnail": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "thumbnail"),
"medium": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "medium"),
"large": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "large"),
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "public"),
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
"is_fallback": True,
}
except Exception:
pass
return None
# Metadata
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
class ParkImageSettingsInputSerializer(serializers.Serializer):
"""Input serializer for setting park 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 park."""
if value is not None:
from apps.parks.models import ParkPhoto
try:
ParkPhoto.objects.get(id=value)
# The park will be validated in the view
return value
except ParkPhoto.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 park."""
if value is not None:
from apps.parks.models import ParkPhoto
try:
ParkPhoto.objects.get(id=value)
# The park will be validated in the view
return value
except ParkPhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
return value
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

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

@@ -1,100 +0,0 @@
"""
Serializers for review-related API endpoints.
"""
from rest_framework import serializers
from apps.parks.models.reviews import ParkReview
from apps.rides.models.reviews import RideReview
from apps.accounts.models import User
class ReviewUserSerializer(serializers.ModelSerializer):
"""Serializer for user information in reviews."""
avatar_url = serializers.SerializerMethodField()
display_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["username", "display_name", "avatar_url"]
def get_avatar_url(self, obj):
"""Get the user's avatar URL."""
if hasattr(obj, "profile") and obj.profile:
return obj.profile.get_avatar_url()
return "/static/images/default-avatar.png"
def get_display_name(self, obj):
"""Get the user's display name."""
return obj.get_display_name()
class LatestReviewSerializer(serializers.Serializer):
"""Serializer for latest reviews combining park and ride reviews."""
id = serializers.IntegerField()
type = serializers.CharField() # 'park' or 'ride'
title = serializers.CharField()
content_snippet = serializers.CharField()
rating = serializers.IntegerField()
created_at = serializers.DateTimeField()
user = ReviewUserSerializer()
# Subject information (park or ride)
subject_name = serializers.CharField()
subject_slug = serializers.CharField()
subject_url = serializers.CharField()
# Park information (for ride reviews)
park_name = serializers.CharField(allow_null=True)
park_slug = serializers.CharField(allow_null=True)
park_url = serializers.CharField(allow_null=True)
def to_representation(self, instance):
"""Convert review instance to serialized representation."""
if isinstance(instance, ParkReview):
return {
"id": instance.pk,
"type": "park",
"title": instance.title,
"content_snippet": self._get_content_snippet(instance.content),
"rating": instance.rating,
"created_at": instance.created_at,
"user": ReviewUserSerializer(instance.user).data,
"subject_name": instance.park.name,
"subject_slug": instance.park.slug,
"subject_url": f"/parks/{instance.park.slug}/",
"park_name": None,
"park_slug": None,
"park_url": None,
}
elif isinstance(instance, RideReview):
return {
"id": instance.pk,
"type": "ride",
"title": instance.title,
"content_snippet": self._get_content_snippet(instance.content),
"rating": instance.rating,
"created_at": instance.created_at,
"user": ReviewUserSerializer(instance.user).data,
"subject_name": instance.ride.name,
"subject_slug": instance.ride.slug,
"subject_url": f"/parks/{instance.ride.park.slug}/rides/{instance.ride.slug}/",
"park_name": instance.ride.park.name,
"park_slug": instance.ride.park.slug,
"park_url": f"/parks/{instance.ride.park.slug}/",
}
return {}
def _get_content_snippet(self, content, max_length=150):
"""Get a snippet of the review content."""
if len(content) <= max_length:
return content
# Find the last complete word within the limit
snippet = content[:max_length]
last_space = snippet.rfind(" ")
if last_space > 0:
snippet = snippet[:last_space]
return snippet + "..."

View File

@@ -1,797 +0,0 @@
"""
RideModel serializers for ThrillWiki API v1.
This module contains all serializers related to ride models, variants,
technical specifications, and related functionality.
"""
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
# Use dynamic imports to avoid circular import issues
def get_ride_model_classes():
"""Get ride model classes dynamically to avoid import issues."""
from apps.rides.models import (
RideModel,
RideModelVariant,
RideModelPhoto,
RideModelTechnicalSpec,
)
return RideModel, RideModelVariant, RideModelPhoto, RideModelTechnicalSpec
# === RIDE MODEL SERIALIZERS ===
class RideModelManufacturerOutputSerializer(serializers.Serializer):
"""Output serializer for ride model's manufacturer data."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
class RideModelPhotoOutputSerializer(serializers.Serializer):
"""Output serializer for ride model photos."""
id = serializers.IntegerField()
image_url = serializers.SerializerMethodField()
caption = serializers.CharField()
alt_text = serializers.CharField()
photo_type = serializers.CharField()
is_primary = serializers.BooleanField()
photographer = serializers.CharField()
source = serializers.CharField()
@extend_schema_field(serializers.URLField(allow_null=True))
def get_image_url(self, obj):
"""Get the image URL."""
if obj.image:
return obj.image.url
return None
class RideModelTechnicalSpecOutputSerializer(serializers.Serializer):
"""Output serializer for ride model technical specifications."""
id = serializers.IntegerField()
spec_category = serializers.CharField()
spec_name = serializers.CharField()
spec_value = serializers.CharField()
spec_unit = serializers.CharField()
notes = serializers.CharField()
class RideModelVariantOutputSerializer(serializers.Serializer):
"""Output serializer for ride model variants."""
id = serializers.IntegerField()
name = serializers.CharField()
description = serializers.CharField()
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
distinguishing_features = serializers.CharField()
@extend_schema_serializer(
examples=[
OpenApiExample(
"Ride Model List Example",
summary="Example ride model list response",
description="A typical ride model in the list view",
value={
"id": 1,
"name": "Hyper Coaster",
"slug": "bolliger-mabillard-hyper-coaster",
"category": "RC",
"description": "High-speed steel roller coaster with airtime hills",
"manufacturer": {
"id": 1,
"name": "Bolliger & Mabillard",
"slug": "bolliger-mabillard",
},
"target_market": "THRILL",
"is_discontinued": False,
"total_installations": 15,
"first_installation_year": 1999,
"height_range_display": "200-325 ft",
"speed_range_display": "70-95 mph",
"primary_image": {
"id": 123,
"image_url": "https://example.com/image.jpg",
"caption": "B&M Hyper Coaster",
"photo_type": "PROMOTIONAL",
},
},
)
]
)
class RideModelListOutputSerializer(serializers.Serializer):
"""Output serializer for ride model list view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
description = serializers.CharField()
# Manufacturer info
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
# Market info
target_market = RichChoiceFieldSerializer(
choice_group="target_markets",
domain="rides"
)
is_discontinued = serializers.BooleanField()
total_installations = serializers.IntegerField()
first_installation_year = serializers.IntegerField(allow_null=True)
last_installation_year = serializers.IntegerField(allow_null=True)
# Display properties
height_range_display = serializers.CharField()
speed_range_display = serializers.CharField()
installation_years_range = serializers.CharField()
# Primary image
primary_image = RideModelPhotoOutputSerializer(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 model."""
return f"{settings.FRONTEND_DOMAIN}/rides/manufacturers/{obj.manufacturer.slug}/{obj.slug}/"
@extend_schema_serializer(
examples=[
OpenApiExample(
"Ride Model Detail Example",
summary="Example ride model detail response",
description="A complete ride model detail response",
value={
"id": 1,
"name": "Hyper Coaster",
"slug": "bolliger-mabillard-hyper-coaster",
"category": "RC",
"description": "High-speed steel roller coaster featuring airtime hills and smooth ride experience",
"manufacturer": {
"id": 1,
"name": "Bolliger & Mabillard",
"slug": "bolliger-mabillard",
},
"typical_height_range_min_ft": 200.0,
"typical_height_range_max_ft": 325.0,
"typical_speed_range_min_mph": 70.0,
"typical_speed_range_max_mph": 95.0,
"typical_capacity_range_min": 1200,
"typical_capacity_range_max": 1800,
"track_type": "Tubular Steel",
"support_structure": "Steel",
"train_configuration": "2-3 trains, 7-9 cars per train, 4 seats per car",
"restraint_system": "Clamshell lap bar",
"target_market": "THRILL",
"is_discontinued": False,
"total_installations": 15,
"first_installation_year": 1999,
"notable_features": "Airtime hills, smooth ride, high capacity",
"photos": [
{
"id": 123,
"image_url": "https://example.com/image.jpg",
"caption": "B&M Hyper Coaster",
"photo_type": "PROMOTIONAL",
"is_primary": True,
}
],
"variants": [
{
"id": 1,
"name": "Mega Coaster",
"description": "200-299 ft height variant",
"min_height_ft": 200.0,
"max_height_ft": 299.0,
}
],
"technical_specs": [
{
"id": 1,
"spec_category": "DIMENSIONS",
"spec_name": "Track Width",
"spec_value": "1435",
"spec_unit": "mm",
}
],
"installations": [
{
"id": 1,
"name": "Nitro",
"park_name": "Six Flags Great Adventure",
"opening_date": "2001-04-07",
}
],
},
)
]
)
class RideModelDetailOutputSerializer(serializers.Serializer):
"""Output serializer for ride model detail view."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = serializers.CharField()
description = serializers.CharField()
# Manufacturer info
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
# Technical specifications
typical_height_range_min_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
typical_height_range_max_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
typical_speed_range_min_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
typical_speed_range_max_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
typical_capacity_range_min = serializers.IntegerField(allow_null=True)
typical_capacity_range_max = serializers.IntegerField(allow_null=True)
# Design characteristics
track_type = serializers.CharField()
support_structure = serializers.CharField()
train_configuration = serializers.CharField()
restraint_system = serializers.CharField()
# Market information
first_installation_year = serializers.IntegerField(allow_null=True)
last_installation_year = serializers.IntegerField(allow_null=True)
is_discontinued = serializers.BooleanField()
total_installations = serializers.IntegerField()
# Design features
notable_features = serializers.CharField()
target_market = serializers.CharField()
# Display properties
height_range_display = serializers.CharField()
speed_range_display = serializers.CharField()
installation_years_range = serializers.CharField()
# SEO metadata
meta_title = serializers.CharField()
meta_description = serializers.CharField()
# Related data
photos = RideModelPhotoOutputSerializer(many=True)
variants = RideModelVariantOutputSerializer(many=True)
technical_specs = RideModelTechnicalSpecOutputSerializer(many=True)
installations = 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 model."""
return f"{settings.FRONTEND_DOMAIN}/rides/manufacturers/{obj.manufacturer.slug}/{obj.slug}/"
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_installations(self, obj):
"""Get ride installations using this model."""
from django.apps import apps
Ride = apps.get_model("rides", "Ride")
installations = Ride.objects.filter(ride_model=obj).select_related("park")[:10]
return [
{
"id": ride.id,
"name": ride.name,
"slug": ride.slug,
"park_name": ride.park.name,
"park_slug": ride.park.slug,
"opening_date": ride.opening_date,
"status": ride.status,
}
for ride in installations
]
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=ModelChoices.get_ride_category_choices(), allow_blank=True, default=""
)
# Required manufacturer
manufacturer_id = serializers.IntegerField()
# Technical specifications
typical_height_range_min_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
typical_height_range_max_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
typical_speed_range_min_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
typical_speed_range_max_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
typical_capacity_range_min = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
typical_capacity_range_max = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
# Design characteristics
track_type = serializers.CharField(max_length=100, allow_blank=True, default="")
support_structure = serializers.CharField(
max_length=100, allow_blank=True, default=""
)
train_configuration = serializers.CharField(
max_length=200, allow_blank=True, default=""
)
restraint_system = serializers.CharField(
max_length=100, allow_blank=True, default=""
)
# Market information
first_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
last_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
is_discontinued = serializers.BooleanField(default=False)
# Design features
notable_features = serializers.CharField(allow_blank=True, default="")
target_market = serializers.ChoiceField(
choices=ModelChoices.get_target_market_choices(),
required=False,
allow_blank=True,
)
def validate(self, attrs):
"""Cross-field validation."""
# Height range validation
min_height = attrs.get("typical_height_range_min_ft")
max_height = attrs.get("typical_height_range_max_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
# Speed range validation
min_speed = attrs.get("typical_speed_range_min_mph")
max_speed = attrs.get("typical_speed_range_max_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
# Capacity range validation
min_capacity = attrs.get("typical_capacity_range_min")
max_capacity = attrs.get("typical_capacity_range_max")
if min_capacity and max_capacity and min_capacity > max_capacity:
raise serializers.ValidationError(
"Minimum capacity cannot be greater than maximum capacity"
)
# Installation years validation
first_year = attrs.get("first_installation_year")
last_year = attrs.get("last_installation_year")
if first_year and last_year and first_year > last_year:
raise serializers.ValidationError(
"First installation year cannot be after last installation year"
)
return attrs
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=ModelChoices.get_ride_category_choices(),
allow_blank=True,
required=False,
)
# Manufacturer
manufacturer_id = serializers.IntegerField(required=False)
# Technical specifications
typical_height_range_min_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
typical_height_range_max_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
typical_speed_range_min_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
typical_speed_range_max_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
typical_capacity_range_min = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
typical_capacity_range_max = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
# Design characteristics
track_type = serializers.CharField(max_length=100, allow_blank=True, required=False)
support_structure = serializers.CharField(
max_length=100, allow_blank=True, required=False
)
train_configuration = serializers.CharField(
max_length=200, allow_blank=True, required=False
)
restraint_system = serializers.CharField(
max_length=100, allow_blank=True, required=False
)
# Market information
first_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
last_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
is_discontinued = serializers.BooleanField(required=False)
# Design features
notable_features = serializers.CharField(allow_blank=True, required=False)
target_market = serializers.ChoiceField(
choices=ModelChoices.get_target_market_choices(),
allow_blank=True,
required=False,
)
def validate(self, attrs):
"""Cross-field validation."""
# Height range validation
min_height = attrs.get("typical_height_range_min_ft")
max_height = attrs.get("typical_height_range_max_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
# Speed range validation
min_speed = attrs.get("typical_speed_range_min_mph")
max_speed = attrs.get("typical_speed_range_max_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
# Capacity range validation
min_capacity = attrs.get("typical_capacity_range_min")
max_capacity = attrs.get("typical_capacity_range_max")
if min_capacity and max_capacity and min_capacity > max_capacity:
raise serializers.ValidationError(
"Minimum capacity cannot be greater than maximum capacity"
)
# Installation years validation
first_year = attrs.get("first_installation_year")
last_year = attrs.get("last_installation_year")
if first_year and last_year and first_year > last_year:
raise serializers.ValidationError(
"First installation year cannot be after last installation year"
)
return attrs
class RideModelFilterInputSerializer(serializers.Serializer):
"""Input serializer for ride model 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
)
# Manufacturer filter
manufacturer_id = serializers.IntegerField(required=False)
manufacturer_slug = serializers.CharField(required=False, allow_blank=True)
# Market filter
target_market = serializers.MultipleChoiceField(
choices=ModelChoices.get_target_market_choices(),
required=False,
)
# Status filter
is_discontinued = serializers.BooleanField(required=False)
# Year filters
first_installation_year_min = serializers.IntegerField(required=False)
first_installation_year_max = serializers.IntegerField(required=False)
# Installation count filter
min_installations = serializers.IntegerField(required=False, min_value=0)
# Height filters
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False
)
# Speed filters
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False
)
# Ordering
ordering = serializers.ChoiceField(
choices=[
"name",
"-name",
"manufacturer__name",
"-manufacturer__name",
"first_installation_year",
"-first_installation_year",
"total_installations",
"-total_installations",
"created_at",
"-created_at",
],
required=False,
default="manufacturer__name,name",
)
# === RIDE MODEL VARIANT SERIALIZERS ===
class RideModelVariantCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating ride model variants."""
ride_model_id = serializers.IntegerField()
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
# Variant-specific specifications
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
# Distinguishing features
distinguishing_features = serializers.CharField(allow_blank=True, default="")
def validate(self, attrs):
"""Cross-field validation."""
# Height range validation
min_height = attrs.get("min_height_ft")
max_height = attrs.get("max_height_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
# Speed range validation
min_speed = attrs.get("min_speed_mph")
max_speed = attrs.get("max_speed_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
return attrs
class RideModelVariantUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating ride model variants."""
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
# Variant-specific specifications
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
# Distinguishing features
distinguishing_features = serializers.CharField(allow_blank=True, required=False)
def validate(self, attrs):
"""Cross-field validation."""
# Height range validation
min_height = attrs.get("min_height_ft")
max_height = attrs.get("max_height_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
# Speed range validation
min_speed = attrs.get("min_speed_mph")
max_speed = attrs.get("max_speed_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
return attrs
# === RIDE MODEL TECHNICAL SPEC SERIALIZERS ===
class RideModelTechnicalSpecCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating ride model technical specifications."""
ride_model_id = serializers.IntegerField()
spec_category = serializers.ChoiceField(
choices=ModelChoices.get_technical_spec_category_choices()
)
spec_name = serializers.CharField(max_length=100)
spec_value = serializers.CharField(max_length=255)
spec_unit = serializers.CharField(max_length=20, allow_blank=True, default="")
notes = serializers.CharField(allow_blank=True, default="")
class RideModelTechnicalSpecUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating ride model technical specifications."""
spec_category = serializers.ChoiceField(
choices=ModelChoices.get_technical_spec_category_choices(),
required=False,
)
spec_name = serializers.CharField(max_length=100, required=False)
spec_value = serializers.CharField(max_length=255, required=False)
spec_unit = serializers.CharField(max_length=20, allow_blank=True, required=False)
notes = serializers.CharField(allow_blank=True, required=False)
# === RIDE MODEL PHOTO SERIALIZERS ===
class RideModelPhotoCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating ride model photos."""
ride_model_id = serializers.IntegerField()
image = serializers.ImageField()
caption = serializers.CharField(max_length=500, allow_blank=True, default="")
alt_text = serializers.CharField(max_length=255, allow_blank=True, default="")
photo_type = serializers.ChoiceField(
choices=ModelChoices.get_photo_type_choices(),
default="PROMOTIONAL",
)
is_primary = serializers.BooleanField(default=False)
photographer = serializers.CharField(max_length=255, allow_blank=True, default="")
source = serializers.CharField(max_length=255, allow_blank=True, default="")
copyright_info = serializers.CharField(max_length=255, allow_blank=True, default="")
class RideModelPhotoUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating ride model photos."""
caption = serializers.CharField(max_length=500, allow_blank=True, required=False)
alt_text = serializers.CharField(max_length=255, allow_blank=True, required=False)
photo_type = serializers.ChoiceField(
choices=ModelChoices.get_photo_type_choices(),
required=False,
)
is_primary = serializers.BooleanField(required=False)
photographer = serializers.CharField(
max_length=255, allow_blank=True, required=False
)
source = serializers.CharField(max_length=255, allow_blank=True, required=False)
copyright_info = serializers.CharField(
max_length=255, allow_blank=True, required=False
)
# === RIDE MODEL STATS SERIALIZERS ===
class RideModelStatsOutputSerializer(serializers.Serializer):
"""Output serializer for ride model statistics."""
total_models = serializers.IntegerField()
total_installations = serializers.IntegerField()
active_manufacturers = serializers.IntegerField()
discontinued_models = serializers.IntegerField()
by_category = serializers.DictField(
child=serializers.IntegerField(), help_text="Model counts by category"
)
by_target_market = serializers.DictField(
child=serializers.IntegerField(), help_text="Model counts by target market"
)
by_manufacturer = serializers.DictField(
child=serializers.IntegerField(), help_text="Model counts by manufacturer"
)
recent_models = serializers.IntegerField(
help_text="Models created in the last 30 days"
)

View File

@@ -1,219 +0,0 @@
"""
Serializers for ride review API endpoints.
This module contains serializers for ride review CRUD operations with Rich Choice Objects compliance.
"""
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer, OpenApiExample
from apps.rides.models.reviews import RideReview
from apps.accounts.models import User
from apps.core.choices.serializers import RichChoiceSerializer
class ReviewUserSerializer(serializers.ModelSerializer):
"""Serializer for user information in ride reviews."""
avatar_url = serializers.SerializerMethodField()
display_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["id", "username", "display_name", "avatar_url"]
read_only_fields = fields
@extend_schema_field(serializers.URLField(allow_null=True))
def get_avatar_url(self, obj):
"""Get the user's avatar URL."""
if hasattr(obj, "profile") and obj.profile:
return obj.profile.get_avatar_url()
return "/static/images/default-avatar.png"
@extend_schema_field(serializers.CharField())
def get_display_name(self, obj):
"""Get the user's display name."""
return obj.get_display_name()
@extend_schema_serializer(
examples=[
OpenApiExample(
name="Complete Ride Review",
summary="Full ride review response",
description="Example response showing all fields for a ride review",
value={
"id": 123,
"title": "Amazing roller coaster experience!",
"content": "This ride was absolutely incredible. The inversions were smooth and the theming was top-notch.",
"rating": 9,
"visit_date": "2023-06-15",
"created_at": "2023-06-16T10:30:00Z",
"updated_at": "2023-06-16T10:30:00Z",
"is_published": True,
"user": {
"id": 456,
"username": "coaster_fan",
"display_name": "Coaster Fan",
"avatar_url": "https://example.com/avatar.jpg"
},
"ride": {
"id": 789,
"name": "Steel Vengeance",
"slug": "steel-vengeance"
},
"park": {
"id": 101,
"name": "Cedar Point",
"slug": "cedar-point"
}
}
)
]
)
class RideReviewOutputSerializer(serializers.ModelSerializer):
"""Output serializer for ride reviews."""
user = ReviewUserSerializer(read_only=True)
# Ride information
ride = serializers.SerializerMethodField()
park = serializers.SerializerMethodField()
class Meta:
model = RideReview
fields = [
"id",
"title",
"content",
"rating",
"visit_date",
"created_at",
"updated_at",
"is_published",
"user",
"ride",
"park",
]
read_only_fields = [
"id",
"created_at",
"updated_at",
"user",
"ride",
"park",
]
@extend_schema_field(serializers.DictField())
def get_ride(self, obj):
"""Get ride information."""
return {
"id": obj.ride.id,
"name": obj.ride.name,
"slug": obj.ride.slug,
}
@extend_schema_field(serializers.DictField())
def get_park(self, obj):
"""Get park information."""
return {
"id": obj.ride.park.id,
"name": obj.ride.park.name,
"slug": obj.ride.park.slug,
}
class RideReviewCreateInputSerializer(serializers.ModelSerializer):
"""Input serializer for creating ride reviews."""
class Meta:
model = RideReview
fields = [
"title",
"content",
"rating",
"visit_date",
]
def validate_rating(self, value):
"""Validate rating is between 1 and 10."""
if not (1 <= value <= 10):
raise serializers.ValidationError("Rating must be between 1 and 10.")
return value
class RideReviewUpdateInputSerializer(serializers.ModelSerializer):
"""Input serializer for updating ride reviews."""
class Meta:
model = RideReview
fields = [
"title",
"content",
"rating",
"visit_date",
]
def validate_rating(self, value):
"""Validate rating is between 1 and 10."""
if not (1 <= value <= 10):
raise serializers.ValidationError("Rating must be between 1 and 10.")
return value
class RideReviewListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for ride review lists."""
user = ReviewUserSerializer(read_only=True)
ride_name = serializers.CharField(source="ride.name", read_only=True)
park_name = serializers.CharField(source="ride.park.name", read_only=True)
class Meta:
model = RideReview
fields = [
"id",
"title",
"rating",
"visit_date",
"created_at",
"is_published",
"user",
"ride_name",
"park_name",
]
read_only_fields = fields
class RideReviewStatsOutputSerializer(serializers.Serializer):
"""Output serializer for ride review statistics."""
total_reviews = serializers.IntegerField()
published_reviews = serializers.IntegerField()
pending_reviews = serializers.IntegerField()
average_rating = serializers.FloatField(allow_null=True)
rating_distribution = serializers.DictField(
child=serializers.IntegerField(),
help_text="Count of reviews by rating (1-10)"
)
recent_reviews = serializers.IntegerField()
class RideReviewModerationInputSerializer(serializers.Serializer):
"""Input serializer for review moderation operations."""
review_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of review IDs to moderate"
)
action = serializers.ChoiceField(
choices=[
("publish", "Publish"),
("unpublish", "Unpublish"),
("delete", "Delete"),
],
help_text="Moderation action to perform"
)
moderation_notes = serializers.CharField(
required=False,
allow_blank=True,
help_text="Optional notes about the moderation action"
)

View File

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

View File

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

@@ -1,92 +0,0 @@
"""
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 ..shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === 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=ModelChoices.get_entity_type_choices()
),
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 = RichChoiceFieldSerializer(
choice_group="entity_types",
domain="core"
)
description = serializers.CharField()
relevance_score = serializers.FloatField()
# Context-specific info — renamed to avoid overriding Serializer.context
context_info = 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

@@ -1,266 +0,0 @@
"""
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_field,
)
# === HEALTH CHECK SERIALIZERS ===
class HealthCheckOutputSerializer(serializers.Serializer):
"""Output serializer for comprehensive health check responses."""
status = serializers.CharField(help_text="Overall health status")
timestamp = serializers.DateTimeField(help_text="Timestamp of health check")
version = serializers.CharField(help_text="Application version")
environment = serializers.CharField(help_text="Environment name")
response_time_ms = serializers.FloatField(help_text="Response time in milliseconds")
checks = serializers.DictField(help_text="Individual health check results")
metrics = serializers.DictField(help_text="System metrics")
class PerformanceMetricsOutputSerializer(serializers.Serializer):
"""Output serializer for performance metrics responses."""
timestamp = serializers.DateTimeField(help_text="Timestamp of metrics collection")
database_analysis = serializers.DictField(help_text="Database performance analysis")
cache_performance = serializers.DictField(help_text="Cache performance metrics")
recent_slow_queries = serializers.DictField(help_text="Recent slow query analysis")
class SimpleHealthOutputSerializer(serializers.Serializer):
"""Output serializer for simple health check responses."""
status = serializers.CharField(help_text="Simple health status")
timestamp = serializers.DateTimeField(help_text="Timestamp of health check")
error = serializers.CharField(
required=False, help_text="Error message if unhealthy"
)
# === 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)
template_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", "Edit Submission"),
("PHOTO", "Photo Submission"),
("REVIEW", "Review Submission"),
],
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, attrs):
"""Validate that park IDs are different."""
if attrs["park1_id"] == attrs["park2_id"]:
raise serializers.ValidationError("Park IDs must be different")
return attrs
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

@@ -1,657 +0,0 @@
"""
Shared Contract Serializers for ThrillWiki API
This module contains standardized serializers that enforce consistent formats
across all API responses, ensuring they match frontend TypeScript interfaces exactly.
These serializers prevent contract violations by providing a single source of truth
for common data structures used throughout the API.
"""
from rest_framework import serializers
from typing import Dict, Any, List
class FilterOptionSerializer(serializers.Serializer):
"""
Standard filter option format - matches frontend TypeScript exactly.
Frontend TypeScript interface:
interface FilterOption {
value: string;
label: string;
count?: number;
selected?: boolean;
}
"""
value = serializers.CharField(
help_text="The actual value used for filtering"
)
label = serializers.CharField(
help_text="Human-readable display label"
)
count = serializers.IntegerField(
required=False,
allow_null=True,
help_text="Number of items matching this filter option"
)
selected = serializers.BooleanField(
default=False,
help_text="Whether this option is currently selected"
)
class FilterRangeSerializer(serializers.Serializer):
"""
Standard range filter format.
Frontend TypeScript interface:
interface FilterRange {
min: number;
max: number;
step: number;
unit?: string;
}
"""
min = serializers.FloatField(
allow_null=True,
help_text="Minimum value for the range"
)
max = serializers.FloatField(
allow_null=True,
help_text="Maximum value for the range"
)
step = serializers.FloatField(
default=1.0,
help_text="Step size for range inputs"
)
unit = serializers.CharField(
required=False,
allow_null=True,
help_text="Unit of measurement (e.g., 'feet', 'mph', 'stars')"
)
class BooleanFilterSerializer(serializers.Serializer):
"""
Standard boolean filter format.
Frontend TypeScript interface:
interface BooleanFilter {
key: string;
label: string;
description: string;
}
"""
key = serializers.CharField(
help_text="The filter parameter key"
)
label = serializers.CharField(
help_text="Human-readable label for the filter"
)
description = serializers.CharField(
help_text="Description of what this filter does"
)
class OrderingOptionSerializer(serializers.Serializer):
"""
Standard ordering option format.
Frontend TypeScript interface:
interface OrderingOption {
value: string;
label: string;
}
"""
value = serializers.CharField(
help_text="The ordering parameter value"
)
label = serializers.CharField(
help_text="Human-readable label for the ordering option"
)
class StandardizedFilterMetadataSerializer(serializers.Serializer):
"""
Matches frontend TypeScript interface exactly.
This serializer ensures all filter metadata responses follow the same structure
that the frontend expects, preventing runtime type errors.
"""
categorical = serializers.DictField(
child=FilterOptionSerializer(many=True),
help_text="Categorical filter options with value/label/count structure"
)
ranges = serializers.DictField(
child=FilterRangeSerializer(),
help_text="Range filter metadata with min/max/step/unit"
)
total_count = serializers.IntegerField(
help_text="Total number of items in the filtered dataset"
)
ordering_options = FilterOptionSerializer(
many=True,
required=False,
help_text="Available ordering options"
)
boolean_filters = BooleanFilterSerializer(
many=True,
required=False,
help_text="Available boolean filter options"
)
class PaginationMetadataSerializer(serializers.Serializer):
"""
Standard pagination metadata format.
Frontend TypeScript interface:
interface PaginationMetadata {
count: number;
next: string | null;
previous: string | null;
page_size: number;
current_page: number;
total_pages: number;
}
"""
count = serializers.IntegerField(
help_text="Total number of items across all pages"
)
next = serializers.URLField(
allow_null=True,
required=False,
help_text="URL for the next page of results"
)
previous = serializers.URLField(
allow_null=True,
required=False,
help_text="URL for the previous page of results"
)
page_size = serializers.IntegerField(
help_text="Number of items per page"
)
current_page = serializers.IntegerField(
help_text="Current page number (1-indexed)"
)
total_pages = serializers.IntegerField(
help_text="Total number of pages"
)
class ApiResponseSerializer(serializers.Serializer):
"""
Standard API response wrapper.
Frontend TypeScript interface:
interface ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
errors?: ValidationError;
}
"""
success = serializers.BooleanField(
help_text="Whether the request was successful"
)
response_data = serializers.JSONField(
required=False,
help_text="Response data (structure varies by endpoint)",
source='data'
)
message = serializers.CharField(
required=False,
help_text="Human-readable message about the operation"
)
response_errors = serializers.DictField(
required=False,
help_text="Validation errors (field -> error messages)",
source='errors'
)
class ErrorResponseSerializer(serializers.Serializer):
"""
Standard error response format.
Frontend TypeScript interface:
interface ApiError {
status: "error";
error: {
code: string;
message: string;
details?: any;
request_user?: string;
};
data: null;
}
"""
status = serializers.CharField(
default="error",
help_text="Response status indicator"
)
error = serializers.DictField(
help_text="Error details"
)
response_data = serializers.JSONField(
default=None,
allow_null=True,
help_text="Always null for error responses",
source='data'
)
class LocationSerializer(serializers.Serializer):
"""
Standard location format.
Frontend TypeScript interface:
interface Location {
city: string;
state?: string;
country: string;
address?: string;
latitude?: number;
longitude?: number;
}
"""
city = serializers.CharField(
help_text="City name"
)
state = serializers.CharField(
required=False,
allow_null=True,
help_text="State/province name"
)
country = serializers.CharField(
help_text="Country name"
)
address = serializers.CharField(
required=False,
allow_null=True,
help_text="Street address"
)
latitude = serializers.FloatField(
required=False,
allow_null=True,
help_text="Latitude coordinate"
)
longitude = serializers.FloatField(
required=False,
allow_null=True,
help_text="Longitude coordinate"
)
# Alias for backward compatibility
LocationOutputSerializer = LocationSerializer
class CompanyOutputSerializer(serializers.Serializer):
"""
Standard company output format.
Frontend TypeScript interface:
interface Company {
id: number;
name: string;
slug: string;
roles?: string[];
}
"""
id = serializers.IntegerField(
help_text="Company ID"
)
name = serializers.CharField(
help_text="Company name"
)
slug = serializers.SlugField(
help_text="URL-friendly identifier"
)
roles = serializers.ListField(
child=serializers.CharField(),
required=False,
help_text="Company roles (manufacturer, operator, etc.)"
)
class ModelChoices:
"""
Utility class to provide model choices for serializers using Rich Choice Objects.
This prevents circular imports while providing access to model choices from the registry.
NO FALLBACKS - All choices must be properly defined in Rich Choice Objects.
"""
@staticmethod
def get_park_status_choices():
"""Get park status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("statuses", "parks")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_ride_status_choices():
"""Get ride status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("statuses", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_company_role_choices():
"""Get company role choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
# Get rides domain company roles (MANUFACTURER, DESIGNER)
rides_choices = get_choices("company_roles", "rides")
# Get parks domain company roles (OPERATOR, PROPERTY_OWNER)
parks_choices = get_choices("company_roles", "parks")
all_choices = list(rides_choices) + list(parks_choices)
return [(choice.value, choice.label) for choice in all_choices]
@staticmethod
def get_ride_category_choices():
"""Get ride category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_ride_post_closing_choices():
"""Get ride post-closing status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("post_closing_statuses", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_coaster_track_choices():
"""Get coaster track material choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("track_materials", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_coaster_type_choices():
"""Get coaster type choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("coaster_types", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_launch_choices():
"""Get launch system choices from Rich Choice registry (legacy method)."""
from apps.core.choices.registry import get_choices
choices = get_choices("propulsion_systems", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_propulsion_system_choices():
"""Get propulsion system choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("propulsion_systems", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_photo_type_choices():
"""Get photo type choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("photo_types", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_spec_category_choices():
"""Get technical specification category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("spec_categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_technical_spec_category_choices():
"""Get technical specification category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("spec_categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_target_market_choices():
"""Get target market choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("target_markets", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_entity_type_choices():
"""Get entity type choices for search functionality."""
from apps.core.choices.registry import get_choices
choices = get_choices("entity_types", "core")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_health_status_choices():
"""Get health check status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("health_statuses", "core")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_simple_health_status_choices():
"""Get simple health check status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("simple_health_statuses", "core")
return [(choice.value, choice.label) for choice in choices]
class EntityReferenceSerializer(serializers.Serializer):
"""
Standard entity reference format.
Frontend TypeScript interface:
interface Entity {
id: number;
name: string;
slug: string;
}
"""
id = serializers.IntegerField(
help_text="Unique identifier"
)
name = serializers.CharField(
help_text="Display name"
)
slug = serializers.SlugField(
help_text="URL-friendly identifier"
)
class ImageVariantsSerializer(serializers.Serializer):
"""
Standard image variants format.
Frontend TypeScript interface:
interface ImageVariants {
thumbnail: string;
medium: string;
large: string;
avatar?: string;
}
"""
thumbnail = serializers.URLField(
help_text="Thumbnail size image URL"
)
medium = serializers.URLField(
help_text="Medium size image URL"
)
large = serializers.URLField(
help_text="Large size image URL"
)
avatar = serializers.URLField(
required=False,
help_text="Avatar size image URL (for user avatars)"
)
class PhotoSerializer(serializers.Serializer):
"""
Standard photo format.
Frontend TypeScript interface:
interface Photo {
id: number;
image_variants: ImageVariants;
alt_text?: string;
image_url?: string;
caption?: string;
photo_type?: string;
uploaded_by?: UserInfo;
uploaded_at?: string;
}
"""
id = serializers.IntegerField(
help_text="Photo ID"
)
image_variants = ImageVariantsSerializer(
help_text="Available image size variants"
)
alt_text = serializers.CharField(
required=False,
allow_null=True,
help_text="Alternative text for accessibility"
)
image_url = serializers.URLField(
required=False,
help_text="Primary image URL (for compatibility)"
)
caption = serializers.CharField(
required=False,
allow_null=True,
help_text="Photo caption"
)
photo_type = serializers.CharField(
required=False,
allow_null=True,
help_text="Type/category of photo"
)
uploaded_by = EntityReferenceSerializer(
required=False,
help_text="User who uploaded the photo"
)
uploaded_at = serializers.DateTimeField(
required=False,
help_text="When the photo was uploaded"
)
class UserInfoSerializer(serializers.Serializer):
"""
Standard user info format.
Frontend TypeScript interface:
interface UserInfo {
id: number;
username: string;
display_name: string;
avatar_url?: string;
}
"""
id = serializers.IntegerField(
help_text="User ID"
)
username = serializers.CharField(
help_text="Username"
)
display_name = serializers.CharField(
help_text="Display name"
)
avatar_url = serializers.URLField(
required=False,
allow_null=True,
help_text="User avatar URL"
)
def validate_filter_metadata_contract(data: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate that filter metadata follows the expected contract.
This function can be used in views to ensure filter metadata
matches the frontend TypeScript interface before returning it.
Args:
data: Filter metadata dictionary
Returns:
Validated and potentially transformed data
Raises:
serializers.ValidationError: If data doesn't match contract
"""
serializer = StandardizedFilterMetadataSerializer(data=data)
serializer.is_valid(raise_exception=True)
# Return validated_data directly - it's already a dict
return serializer.validated_data
def ensure_filter_option_format(options: List[Any]) -> List[Dict[str, Any]]:
"""
Ensure a list of filter options follows the expected format.
This utility function converts various input formats to the standard
FilterOption format expected by the frontend.
Args:
options: List of options in various formats
Returns:
List of options in standard format
"""
standardized = []
for option in options:
if isinstance(option, dict):
# Already in correct format or close to it
standardized_option = {
'value': str(option.get('value', option.get('id', ''))),
'label': option.get('label', option.get('name', str(option.get('value', '')))),
'count': option.get('count'),
'selected': option.get('selected', False)
}
elif hasattr(option, 'value') and hasattr(option, 'label'):
# RichChoice object format
standardized_option = {
'value': str(option.value),
'label': str(option.label),
'count': None,
'selected': False
}
else:
# Simple value - use as both value and label
standardized_option = {
'value': str(option),
'label': str(option),
'count': None,
'selected': False
}
standardized.append(standardized_option)
return standardized
def ensure_range_format(range_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Ensure range data follows the expected format.
Args:
range_data: Range data dictionary
Returns:
Range data in standard format
"""
return {
'min': range_data.get('min'),
'max': range_data.get('max'),
'step': range_data.get('step', 1.0),
'unit': range_data.get('unit')
}

View File

@@ -1,135 +0,0 @@
"""
Statistics serializers for ThrillWiki API.
Provides serialization for platform statistics data.
"""
from rest_framework import serializers
class StatsSerializer(serializers.Serializer):
"""
Serializer for platform statistics response.
This serializer defines the structure of the statistics API response,
including all the various counts and breakdowns available.
"""
# Core entity counts
total_parks = serializers.IntegerField(
help_text="Total number of parks in the database"
)
total_rides = serializers.IntegerField(
help_text="Total number of rides in the database"
)
total_manufacturers = serializers.IntegerField(
help_text="Total number of ride manufacturers"
)
total_operators = serializers.IntegerField(
help_text="Total number of park operators"
)
total_designers = serializers.IntegerField(
help_text="Total number of ride designers"
)
total_property_owners = serializers.IntegerField(
help_text="Total number of property owners"
)
total_roller_coasters = serializers.IntegerField(
help_text="Total number of roller coasters with detailed stats"
)
# Photo counts
total_photos = serializers.IntegerField(
help_text="Total number of photos (parks + rides combined)"
)
total_park_photos = serializers.IntegerField(
help_text="Total number of park photos"
)
total_ride_photos = serializers.IntegerField(
help_text="Total number of ride photos"
)
# Review counts
total_reviews = serializers.IntegerField(
help_text="Total number of reviews (parks + rides)"
)
total_park_reviews = serializers.IntegerField(
help_text="Total number of park reviews"
)
total_ride_reviews = serializers.IntegerField(
help_text="Total number of ride reviews"
)
# Ride category counts (optional fields since they depend on data)
roller_coasters = serializers.IntegerField(
required=False, help_text="Number of rides categorized as roller coasters"
)
dark_rides = serializers.IntegerField(
required=False, help_text="Number of rides categorized as dark rides"
)
flat_rides = serializers.IntegerField(
required=False, help_text="Number of rides categorized as flat rides"
)
water_rides = serializers.IntegerField(
required=False, help_text="Number of rides categorized as water rides"
)
transport_rides = serializers.IntegerField(
required=False, help_text="Number of rides categorized as transport rides"
)
other_rides = serializers.IntegerField(
required=False, help_text="Number of rides categorized as other"
)
# Park status counts (optional fields since they depend on data)
operating_parks = serializers.IntegerField(
required=False, help_text="Number of currently operating parks"
)
temporarily_closed_parks = serializers.IntegerField(
required=False, help_text="Number of temporarily closed parks"
)
permanently_closed_parks = serializers.IntegerField(
required=False, help_text="Number of permanently closed parks"
)
under_construction_parks = serializers.IntegerField(
required=False, help_text="Number of parks under construction"
)
demolished_parks = serializers.IntegerField(
required=False, help_text="Number of demolished parks"
)
relocated_parks = serializers.IntegerField(
required=False, help_text="Number of relocated parks"
)
# Ride status counts (optional fields since they depend on data)
operating_rides = serializers.IntegerField(
required=False, help_text="Number of currently operating rides"
)
temporarily_closed_rides = serializers.IntegerField(
required=False, help_text="Number of temporarily closed rides"
)
sbno_rides = serializers.IntegerField(
required=False, help_text="Number of rides standing but not operating"
)
closing_rides = serializers.IntegerField(
required=False, help_text="Number of rides in the process of closing"
)
permanently_closed_rides = serializers.IntegerField(
required=False, help_text="Number of permanently closed rides"
)
under_construction_rides = serializers.IntegerField(
required=False, help_text="Number of rides under construction"
)
demolished_rides = serializers.IntegerField(
required=False, help_text="Number of demolished rides"
)
relocated_rides = serializers.IntegerField(
required=False, help_text="Number of relocated rides"
)
# Metadata
last_updated = serializers.CharField(
help_text="ISO timestamp when these statistics were last calculated"
)
relative_last_updated = serializers.CharField(
help_text="Human-readable relative time since last update (e.g., '2 minutes ago')"
)