mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 17:51:08 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
294
backend/apps/api/v1/serializers/__init__.py
Normal file
294
backend/apps/api/v1/serializers/__init__.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
ThrillWiki API v1 serializers module.
|
||||
|
||||
This module provides a unified interface to all serializers across different domains
|
||||
while maintaining the modular structure for better organization and maintainability.
|
||||
"""
|
||||
|
||||
# Shared utilities and base classes
|
||||
from .shared import (
|
||||
CATEGORY_CHOICES,
|
||||
ModelChoices,
|
||||
LocationOutputSerializer,
|
||||
CompanyOutputSerializer,
|
||||
UserModel,
|
||||
)
|
||||
|
||||
# Parks domain
|
||||
from .parks import (
|
||||
ParkListOutputSerializer,
|
||||
ParkDetailOutputSerializer,
|
||||
ParkCreateInputSerializer,
|
||||
ParkUpdateInputSerializer,
|
||||
ParkFilterInputSerializer,
|
||||
ParkAreaDetailOutputSerializer,
|
||||
ParkAreaCreateInputSerializer,
|
||||
ParkAreaUpdateInputSerializer,
|
||||
ParkLocationOutputSerializer,
|
||||
ParkLocationCreateInputSerializer,
|
||||
ParkLocationUpdateInputSerializer,
|
||||
ParkSuggestionSerializer,
|
||||
ParkSuggestionOutputSerializer,
|
||||
)
|
||||
|
||||
# Companies and ride models domain
|
||||
from .companies import (
|
||||
CompanyDetailOutputSerializer,
|
||||
CompanyCreateInputSerializer,
|
||||
CompanyUpdateInputSerializer,
|
||||
RideModelDetailOutputSerializer,
|
||||
RideModelCreateInputSerializer,
|
||||
RideModelUpdateInputSerializer,
|
||||
)
|
||||
|
||||
# Rides domain
|
||||
from .rides import (
|
||||
RideParkOutputSerializer,
|
||||
RideModelOutputSerializer,
|
||||
RideListOutputSerializer,
|
||||
RideDetailOutputSerializer,
|
||||
RideCreateInputSerializer,
|
||||
RideUpdateInputSerializer,
|
||||
RideFilterInputSerializer,
|
||||
RollerCoasterStatsOutputSerializer,
|
||||
RollerCoasterStatsCreateInputSerializer,
|
||||
RollerCoasterStatsUpdateInputSerializer,
|
||||
RideLocationOutputSerializer,
|
||||
RideLocationCreateInputSerializer,
|
||||
RideLocationUpdateInputSerializer,
|
||||
RideReviewOutputSerializer,
|
||||
RideReviewCreateInputSerializer,
|
||||
RideReviewUpdateInputSerializer,
|
||||
)
|
||||
|
||||
# Accounts domain
|
||||
from .accounts import (
|
||||
UserProfileOutputSerializer,
|
||||
UserProfileCreateInputSerializer,
|
||||
UserProfileUpdateInputSerializer,
|
||||
TopListOutputSerializer,
|
||||
TopListCreateInputSerializer,
|
||||
TopListUpdateInputSerializer,
|
||||
TopListItemOutputSerializer,
|
||||
TopListItemCreateInputSerializer,
|
||||
TopListItemUpdateInputSerializer,
|
||||
UserOutputSerializer,
|
||||
LoginInputSerializer,
|
||||
LoginOutputSerializer,
|
||||
SignupInputSerializer,
|
||||
SignupOutputSerializer,
|
||||
PasswordResetInputSerializer,
|
||||
PasswordResetOutputSerializer,
|
||||
PasswordChangeInputSerializer,
|
||||
PasswordChangeOutputSerializer,
|
||||
LogoutOutputSerializer,
|
||||
SocialProviderOutputSerializer,
|
||||
AuthStatusOutputSerializer,
|
||||
)
|
||||
|
||||
# Statistics and health checks
|
||||
from .other import (
|
||||
ParkStatsOutputSerializer,
|
||||
RideStatsOutputSerializer,
|
||||
ParkReviewOutputSerializer,
|
||||
HealthCheckOutputSerializer,
|
||||
PerformanceMetricsOutputSerializer,
|
||||
SimpleHealthOutputSerializer,
|
||||
)
|
||||
|
||||
# Media domain
|
||||
from .media import (
|
||||
PhotoUploadInputSerializer,
|
||||
PhotoDetailOutputSerializer,
|
||||
PhotoListOutputSerializer,
|
||||
PhotoUpdateInputSerializer,
|
||||
)
|
||||
|
||||
# Parks media domain
|
||||
from .parks_media import (
|
||||
ParkPhotoOutputSerializer,
|
||||
ParkPhotoCreateInputSerializer,
|
||||
ParkPhotoUpdateInputSerializer,
|
||||
ParkPhotoListOutputSerializer,
|
||||
ParkPhotoApprovalInputSerializer,
|
||||
ParkPhotoStatsOutputSerializer,
|
||||
)
|
||||
|
||||
# Rides media domain
|
||||
from .rides_media import (
|
||||
RidePhotoOutputSerializer,
|
||||
RidePhotoCreateInputSerializer,
|
||||
RidePhotoUpdateInputSerializer,
|
||||
RidePhotoListOutputSerializer,
|
||||
RidePhotoApprovalInputSerializer,
|
||||
RidePhotoStatsOutputSerializer,
|
||||
RidePhotoTypeFilterSerializer,
|
||||
)
|
||||
|
||||
# Search domain
|
||||
from .search import (
|
||||
EntitySearchInputSerializer,
|
||||
EntitySearchResultSerializer,
|
||||
EntitySearchOutputSerializer,
|
||||
LocationSearchResultSerializer,
|
||||
LocationSearchOutputSerializer,
|
||||
ReverseGeocodeOutputSerializer,
|
||||
)
|
||||
|
||||
# History domain
|
||||
from .history import (
|
||||
ParkHistoryEventSerializer,
|
||||
RideHistoryEventSerializer,
|
||||
ParkHistoryOutputSerializer,
|
||||
RideHistoryOutputSerializer,
|
||||
UnifiedHistoryTimelineSerializer,
|
||||
HistorySummarySerializer,
|
||||
)
|
||||
|
||||
# Services domain
|
||||
from .services import (
|
||||
EmailSendInputSerializer,
|
||||
EmailTemplateOutputSerializer,
|
||||
MapDataOutputSerializer,
|
||||
CoordinateInputSerializer,
|
||||
HistoryEventSerializer,
|
||||
HistoryEntryOutputSerializer,
|
||||
HistoryCreateInputSerializer,
|
||||
ModerationSubmissionSerializer,
|
||||
ModerationSubmissionOutputSerializer,
|
||||
RoadtripParkSerializer,
|
||||
RoadtripCreateInputSerializer,
|
||||
RoadtripOutputSerializer,
|
||||
GeocodeInputSerializer,
|
||||
GeocodeOutputSerializer,
|
||||
DistanceCalculationInputSerializer,
|
||||
DistanceCalculationOutputSerializer,
|
||||
)
|
||||
|
||||
# Re-export everything for backward compatibility
|
||||
__all__ = [
|
||||
# Shared
|
||||
"CATEGORY_CHOICES",
|
||||
"ModelChoices",
|
||||
"LocationOutputSerializer",
|
||||
"CompanyOutputSerializer",
|
||||
"UserModel",
|
||||
# Parks
|
||||
"ParkListOutputSerializer",
|
||||
"ParkDetailOutputSerializer",
|
||||
"ParkCreateInputSerializer",
|
||||
"ParkUpdateInputSerializer",
|
||||
"ParkFilterInputSerializer",
|
||||
"ParkAreaDetailOutputSerializer",
|
||||
"ParkAreaCreateInputSerializer",
|
||||
"ParkAreaUpdateInputSerializer",
|
||||
"ParkLocationOutputSerializer",
|
||||
"ParkLocationCreateInputSerializer",
|
||||
"ParkLocationUpdateInputSerializer",
|
||||
"ParkSuggestionSerializer",
|
||||
"ParkSuggestionOutputSerializer",
|
||||
# Companies
|
||||
"CompanyDetailOutputSerializer",
|
||||
"CompanyCreateInputSerializer",
|
||||
"CompanyUpdateInputSerializer",
|
||||
"RideModelDetailOutputSerializer",
|
||||
"RideModelCreateInputSerializer",
|
||||
"RideModelUpdateInputSerializer",
|
||||
# Rides
|
||||
"RideParkOutputSerializer",
|
||||
"RideModelOutputSerializer",
|
||||
"RideListOutputSerializer",
|
||||
"RideDetailOutputSerializer",
|
||||
"RideCreateInputSerializer",
|
||||
"RideUpdateInputSerializer",
|
||||
"RideFilterInputSerializer",
|
||||
"RollerCoasterStatsOutputSerializer",
|
||||
"RollerCoasterStatsCreateInputSerializer",
|
||||
"RollerCoasterStatsUpdateInputSerializer",
|
||||
"RideLocationOutputSerializer",
|
||||
"RideLocationCreateInputSerializer",
|
||||
"RideLocationUpdateInputSerializer",
|
||||
"RideReviewOutputSerializer",
|
||||
"RideReviewCreateInputSerializer",
|
||||
"RideReviewUpdateInputSerializer",
|
||||
# Services
|
||||
"EmailSendInputSerializer",
|
||||
"EmailTemplateOutputSerializer",
|
||||
"MapDataOutputSerializer",
|
||||
"CoordinateInputSerializer",
|
||||
"HistoryEventSerializer",
|
||||
"HistoryEntryOutputSerializer",
|
||||
"HistoryCreateInputSerializer",
|
||||
"ModerationSubmissionSerializer",
|
||||
"ModerationSubmissionOutputSerializer",
|
||||
"RoadtripParkSerializer",
|
||||
"RoadtripCreateInputSerializer",
|
||||
"RoadtripOutputSerializer",
|
||||
"GeocodeInputSerializer",
|
||||
"GeocodeOutputSerializer",
|
||||
"DistanceCalculationInputSerializer",
|
||||
"DistanceCalculationOutputSerializer",
|
||||
# Media
|
||||
"PhotoUploadInputSerializer",
|
||||
"PhotoDetailOutputSerializer",
|
||||
"PhotoListOutputSerializer",
|
||||
"PhotoUpdateInputSerializer",
|
||||
# Parks Media
|
||||
"ParkPhotoOutputSerializer",
|
||||
"ParkPhotoCreateInputSerializer",
|
||||
"ParkPhotoUpdateInputSerializer",
|
||||
"ParkPhotoListOutputSerializer",
|
||||
"ParkPhotoApprovalInputSerializer",
|
||||
"ParkPhotoStatsOutputSerializer",
|
||||
# Rides Media
|
||||
"RidePhotoOutputSerializer",
|
||||
"RidePhotoCreateInputSerializer",
|
||||
"RidePhotoUpdateInputSerializer",
|
||||
"RidePhotoListOutputSerializer",
|
||||
"RidePhotoApprovalInputSerializer",
|
||||
"RidePhotoStatsOutputSerializer",
|
||||
"RidePhotoTypeFilterSerializer",
|
||||
# Search
|
||||
"EntitySearchInputSerializer",
|
||||
"EntitySearchResultSerializer",
|
||||
"EntitySearchOutputSerializer",
|
||||
"LocationSearchResultSerializer",
|
||||
"LocationSearchOutputSerializer",
|
||||
"ReverseGeocodeOutputSerializer",
|
||||
# History
|
||||
"ParkHistoryEventSerializer",
|
||||
"RideHistoryEventSerializer",
|
||||
"ParkHistoryOutputSerializer",
|
||||
"RideHistoryOutputSerializer",
|
||||
"UnifiedHistoryTimelineSerializer",
|
||||
"HistorySummarySerializer",
|
||||
# Statistics and health
|
||||
"ParkStatsOutputSerializer",
|
||||
"RideStatsOutputSerializer",
|
||||
"ParkReviewOutputSerializer",
|
||||
"HealthCheckOutputSerializer",
|
||||
"PerformanceMetricsOutputSerializer",
|
||||
"SimpleHealthOutputSerializer",
|
||||
# Accounts
|
||||
"UserProfileOutputSerializer",
|
||||
"UserProfileCreateInputSerializer",
|
||||
"UserProfileUpdateInputSerializer",
|
||||
"TopListOutputSerializer",
|
||||
"TopListCreateInputSerializer",
|
||||
"TopListUpdateInputSerializer",
|
||||
"TopListItemOutputSerializer",
|
||||
"TopListItemCreateInputSerializer",
|
||||
"TopListItemUpdateInputSerializer",
|
||||
"UserOutputSerializer",
|
||||
"LoginInputSerializer",
|
||||
"LoginOutputSerializer",
|
||||
"SignupInputSerializer",
|
||||
"SignupOutputSerializer",
|
||||
"PasswordResetInputSerializer",
|
||||
"PasswordResetOutputSerializer",
|
||||
"PasswordChangeInputSerializer",
|
||||
"PasswordChangeOutputSerializer",
|
||||
"LogoutOutputSerializer",
|
||||
"SocialProviderOutputSerializer",
|
||||
"AuthStatusOutputSerializer",
|
||||
]
|
||||
496
backend/apps/api/v1/serializers/accounts.py
Normal file
496
backend/apps/api/v1/serializers/accounts.py
Normal file
@@ -0,0 +1,496 @@
|
||||
"""
|
||||
Accounts domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains all serializers related to user accounts, profiles,
|
||||
authentication, top lists, and user statistics.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from .shared import UserModel, ModelChoices
|
||||
|
||||
|
||||
# === USER PROFILE SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"User Profile Example",
|
||||
summary="Example user profile response",
|
||||
description="A user's profile information",
|
||||
value={
|
||||
"id": 1,
|
||||
"profile_id": "1234",
|
||||
"display_name": "Coaster Enthusiast",
|
||||
"bio": "Love visiting theme parks around the world!",
|
||||
"pronouns": "they/them",
|
||||
"avatar_url": "/media/avatars/user1.jpg",
|
||||
"coaster_credits": 150,
|
||||
"dark_ride_credits": 45,
|
||||
"flat_ride_credits": 80,
|
||||
"water_ride_credits": 25,
|
||||
"user": {
|
||||
"username": "coaster_fan",
|
||||
"date_joined": "2024-01-01T00:00:00Z",
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class UserProfileOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for user profiles."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
profile_id = serializers.CharField()
|
||||
display_name = serializers.CharField()
|
||||
bio = serializers.CharField()
|
||||
pronouns = serializers.CharField()
|
||||
avatar_url = serializers.SerializerMethodField()
|
||||
twitter = serializers.URLField()
|
||||
instagram = serializers.URLField()
|
||||
youtube = serializers.URLField()
|
||||
discord = serializers.CharField()
|
||||
|
||||
# Ride statistics
|
||||
coaster_credits = serializers.IntegerField()
|
||||
dark_ride_credits = serializers.IntegerField()
|
||||
flat_ride_credits = serializers.IntegerField()
|
||||
water_ride_credits = serializers.IntegerField()
|
||||
|
||||
# User info (limited)
|
||||
user = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.URLField(allow_null=True))
|
||||
def get_avatar_url(self, obj) -> str | None:
|
||||
return obj.get_avatar()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_user(self, obj) -> dict:
|
||||
return {
|
||||
"username": obj.user.username,
|
||||
"date_joined": obj.user.date_joined,
|
||||
}
|
||||
|
||||
|
||||
class UserProfileCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating user profiles."""
|
||||
|
||||
display_name = serializers.CharField(max_length=50)
|
||||
bio = serializers.CharField(max_length=500, allow_blank=True, default="")
|
||||
pronouns = serializers.CharField(max_length=50, allow_blank=True, default="")
|
||||
twitter = serializers.URLField(required=False, allow_blank=True)
|
||||
instagram = serializers.URLField(required=False, allow_blank=True)
|
||||
youtube = serializers.URLField(required=False, allow_blank=True)
|
||||
discord = serializers.CharField(max_length=100, allow_blank=True, default="")
|
||||
|
||||
|
||||
class UserProfileUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating user profiles."""
|
||||
|
||||
display_name = serializers.CharField(max_length=50, required=False)
|
||||
bio = serializers.CharField(max_length=500, allow_blank=True, required=False)
|
||||
pronouns = serializers.CharField(max_length=50, allow_blank=True, required=False)
|
||||
twitter = serializers.URLField(required=False, allow_blank=True)
|
||||
instagram = serializers.URLField(required=False, allow_blank=True)
|
||||
youtube = serializers.URLField(required=False, allow_blank=True)
|
||||
discord = serializers.CharField(max_length=100, allow_blank=True, required=False)
|
||||
coaster_credits = serializers.IntegerField(required=False)
|
||||
dark_ride_credits = serializers.IntegerField(required=False)
|
||||
flat_ride_credits = serializers.IntegerField(required=False)
|
||||
water_ride_credits = serializers.IntegerField(required=False)
|
||||
|
||||
|
||||
# === TOP LIST SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Top List Example",
|
||||
summary="Example top list response",
|
||||
description="A user's top list of rides or parks",
|
||||
value={
|
||||
"id": 1,
|
||||
"title": "My Top 10 Roller Coasters",
|
||||
"category": "RC",
|
||||
"description": "My favorite roller coasters ranked",
|
||||
"user": {"username": "coaster_fan", "display_name": "Coaster Fan"},
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-08-15T12:00:00Z",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class TopListOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for top lists."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
title = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
# User info
|
||||
user = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_user(self, obj) -> dict:
|
||||
return {
|
||||
"username": obj.user.username,
|
||||
"display_name": obj.user.get_display_name(),
|
||||
}
|
||||
|
||||
|
||||
class TopListCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating top lists."""
|
||||
|
||||
title = serializers.CharField(max_length=100)
|
||||
category = serializers.ChoiceField(choices=ModelChoices.get_top_list_categories())
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
|
||||
|
||||
class TopListUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating top lists."""
|
||||
|
||||
title = serializers.CharField(max_length=100, required=False)
|
||||
category = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_top_list_categories(), required=False
|
||||
)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
|
||||
|
||||
# === TOP LIST ITEM SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Top List Item Example",
|
||||
summary="Example top list item response",
|
||||
description="An item in a user's top list",
|
||||
value={
|
||||
"id": 1,
|
||||
"rank": 1,
|
||||
"notes": "Amazing airtime and smooth ride",
|
||||
"object_name": "Steel Vengeance",
|
||||
"object_type": "Ride",
|
||||
"top_list": {"id": 1, "title": "My Top 10 Roller Coasters"},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class TopListItemOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for top list items."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
rank = serializers.IntegerField()
|
||||
notes = serializers.CharField()
|
||||
object_name = serializers.SerializerMethodField()
|
||||
object_type = serializers.SerializerMethodField()
|
||||
|
||||
# Top list info
|
||||
top_list = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.CharField())
|
||||
def get_object_name(self, obj) -> str:
|
||||
"""Get the name of the referenced object."""
|
||||
# This would need to be implemented based on the generic foreign key
|
||||
return "Object Name" # Placeholder
|
||||
|
||||
@extend_schema_field(serializers.CharField())
|
||||
def get_object_type(self, obj) -> str:
|
||||
"""Get the type of the referenced object."""
|
||||
return obj.content_type.model_class().__name__
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_top_list(self, obj) -> dict:
|
||||
return {
|
||||
"id": obj.top_list.id,
|
||||
"title": obj.top_list.title,
|
||||
}
|
||||
|
||||
|
||||
class TopListItemCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating top list items."""
|
||||
|
||||
top_list_id = serializers.IntegerField()
|
||||
content_type_id = serializers.IntegerField()
|
||||
object_id = serializers.IntegerField()
|
||||
rank = serializers.IntegerField(min_value=1)
|
||||
notes = serializers.CharField(allow_blank=True, default="")
|
||||
|
||||
|
||||
class TopListItemUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating top list items."""
|
||||
|
||||
rank = serializers.IntegerField(min_value=1, required=False)
|
||||
notes = serializers.CharField(allow_blank=True, required=False)
|
||||
|
||||
|
||||
# === ACCOUNTS SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"User Example",
|
||||
summary="Example user response",
|
||||
description="A typical user object",
|
||||
value={
|
||||
"id": 1,
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"date_joined": "2024-01-01T12:00:00Z",
|
||||
"is_active": True,
|
||||
"avatar_url": "https://example.com/avatars/john.jpg",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class UserOutputSerializer(serializers.ModelSerializer):
|
||||
"""User serializer for API responses."""
|
||||
|
||||
avatar_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = UserModel
|
||||
fields = [
|
||||
"id",
|
||||
"username",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"date_joined",
|
||||
"is_active",
|
||||
"avatar_url",
|
||||
]
|
||||
read_only_fields = ["id", "date_joined", "is_active"]
|
||||
|
||||
@extend_schema_field(serializers.URLField(allow_null=True))
|
||||
def get_avatar_url(self, obj) -> str | None:
|
||||
"""Get user avatar URL."""
|
||||
if hasattr(obj, "profile") and obj.profile.avatar:
|
||||
return obj.profile.avatar.url
|
||||
return None
|
||||
|
||||
|
||||
class LoginInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for user login."""
|
||||
|
||||
username = serializers.CharField(
|
||||
max_length=254, help_text="Username or email address"
|
||||
)
|
||||
password = serializers.CharField(
|
||||
max_length=128, style={"input_type": "password"}, trim_whitespace=False
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
username = attrs.get("username")
|
||||
password = attrs.get("password")
|
||||
|
||||
if username and password:
|
||||
return attrs
|
||||
|
||||
raise serializers.ValidationError("Must include username/email and password.")
|
||||
|
||||
|
||||
class LoginOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for successful login."""
|
||||
|
||||
token = serializers.CharField()
|
||||
user = UserOutputSerializer()
|
||||
message = serializers.CharField()
|
||||
|
||||
|
||||
class SignupInputSerializer(serializers.ModelSerializer):
|
||||
"""Input serializer for user registration."""
|
||||
|
||||
password = serializers.CharField(
|
||||
write_only=True,
|
||||
validators=[validate_password],
|
||||
style={"input_type": "password"},
|
||||
)
|
||||
password_confirm = serializers.CharField(
|
||||
write_only=True, style={"input_type": "password"}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserModel
|
||||
fields = [
|
||||
"username",
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"password",
|
||||
"password_confirm",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"password": {"write_only": True},
|
||||
"email": {"required": True},
|
||||
}
|
||||
|
||||
def validate_email(self, value):
|
||||
"""Validate email is unique."""
|
||||
if UserModel.objects.filter(email=value).exists():
|
||||
raise serializers.ValidationError("A user with this email already exists.")
|
||||
return value
|
||||
|
||||
def validate_username(self, value):
|
||||
"""Validate username is unique."""
|
||||
if UserModel.objects.filter(username=value).exists():
|
||||
raise serializers.ValidationError(
|
||||
"A user with this username already exists."
|
||||
)
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Validate passwords match."""
|
||||
password = attrs.get("password")
|
||||
password_confirm = attrs.get("password_confirm")
|
||||
|
||||
if password != password_confirm:
|
||||
raise serializers.ValidationError(
|
||||
{"password_confirm": "Passwords do not match."}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create user with validated data."""
|
||||
validated_data.pop("password_confirm", None)
|
||||
password = validated_data.pop("password")
|
||||
|
||||
# Use type: ignore for Django's create_user method which isn't properly typed
|
||||
user = UserModel.objects.create_user( # type: ignore[attr-defined]
|
||||
password=password, **validated_data
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class SignupOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for successful signup."""
|
||||
|
||||
token = serializers.CharField()
|
||||
user = UserOutputSerializer()
|
||||
message = serializers.CharField()
|
||||
|
||||
|
||||
class PasswordResetInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for password reset request."""
|
||||
|
||||
email = serializers.EmailField()
|
||||
|
||||
def validate_email(self, value):
|
||||
"""Validate email exists."""
|
||||
try:
|
||||
user = UserModel.objects.get(email=value)
|
||||
self.user = user
|
||||
return value
|
||||
except UserModel.DoesNotExist:
|
||||
# Don't reveal if email exists or not for security
|
||||
return value
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""Send password reset email if user exists."""
|
||||
if hasattr(self, "user"):
|
||||
# Create password reset token
|
||||
token = get_random_string(64)
|
||||
# Note: PasswordReset model would need to be imported
|
||||
# PasswordReset.objects.update_or_create(...)
|
||||
pass
|
||||
|
||||
|
||||
class PasswordResetOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for password reset request."""
|
||||
|
||||
detail = serializers.CharField()
|
||||
|
||||
|
||||
class PasswordChangeInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for password change."""
|
||||
|
||||
old_password = serializers.CharField(
|
||||
max_length=128, style={"input_type": "password"}
|
||||
)
|
||||
new_password = serializers.CharField(
|
||||
max_length=128,
|
||||
validators=[validate_password],
|
||||
style={"input_type": "password"},
|
||||
)
|
||||
new_password_confirm = serializers.CharField(
|
||||
max_length=128, style={"input_type": "password"}
|
||||
)
|
||||
|
||||
def validate_old_password(self, value):
|
||||
"""Validate old password is correct."""
|
||||
user = self.context["request"].user
|
||||
if not user.check_password(value):
|
||||
raise serializers.ValidationError("Old password is incorrect.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Validate new passwords match."""
|
||||
new_password = attrs.get("new_password")
|
||||
new_password_confirm = attrs.get("new_password_confirm")
|
||||
|
||||
if new_password != new_password_confirm:
|
||||
raise serializers.ValidationError(
|
||||
{"new_password_confirm": "New passwords do not match."}
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""Change user password."""
|
||||
user = self.context["request"].user
|
||||
# validated_data is guaranteed to exist after is_valid() is called
|
||||
new_password = self.validated_data["new_password"] # type: ignore[index]
|
||||
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class PasswordChangeOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for password change."""
|
||||
|
||||
detail = serializers.CharField()
|
||||
|
||||
|
||||
class LogoutOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for logout."""
|
||||
|
||||
message = serializers.CharField()
|
||||
|
||||
|
||||
class SocialProviderOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for social authentication providers."""
|
||||
|
||||
id = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
authUrl = serializers.URLField()
|
||||
|
||||
|
||||
class AuthStatusOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for authentication status check."""
|
||||
|
||||
authenticated = serializers.BooleanField()
|
||||
user = UserOutputSerializer(allow_null=True)
|
||||
149
backend/apps/api/v1/serializers/companies.py
Normal file
149
backend/apps/api/v1/serializers/companies.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Companies and ride models domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains all serializers related to companies that operate parks
|
||||
or manufacture rides, as well as ride model serializers.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
from .shared import CATEGORY_CHOICES, ModelChoices
|
||||
|
||||
|
||||
# === COMPANY SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Company Example",
|
||||
summary="Example company response",
|
||||
description="A company that operates parks or manufactures rides",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Cedar Fair",
|
||||
"slug": "cedar-fair",
|
||||
"roles": ["OPERATOR", "PROPERTY_OWNER"],
|
||||
"description": "Theme park operator based in Ohio",
|
||||
"website": "https://cedarfair.com",
|
||||
"founded_date": "1983-01-01",
|
||||
"rides_count": 0,
|
||||
"coasters_count": 0,
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class CompanyDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for company details."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
roles = serializers.ListField(child=serializers.CharField())
|
||||
description = serializers.CharField()
|
||||
website = serializers.URLField()
|
||||
founded_date = serializers.DateField(allow_null=True)
|
||||
rides_count = serializers.IntegerField()
|
||||
coasters_count = serializers.IntegerField()
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
class CompanyCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating companies."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
roles = serializers.ListField(
|
||||
child=serializers.ChoiceField(choices=ModelChoices.get_company_role_choices()),
|
||||
allow_empty=False,
|
||||
)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
website = serializers.URLField(required=False, allow_blank=True)
|
||||
founded_date = serializers.DateField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class CompanyUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating companies."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
roles = serializers.ListField(
|
||||
child=serializers.ChoiceField(choices=ModelChoices.get_company_role_choices()),
|
||||
required=False,
|
||||
)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
website = serializers.URLField(required=False, allow_blank=True)
|
||||
founded_date = serializers.DateField(required=False, allow_null=True)
|
||||
|
||||
|
||||
# === RIDE MODEL SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Ride Model Example",
|
||||
summary="Example ride model response",
|
||||
description="A specific model/type of ride manufactured by a company",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Dive Coaster",
|
||||
"description": "A roller coaster featuring a near-vertical drop",
|
||||
"category": "RC",
|
||||
"manufacturer": {
|
||||
"id": 1,
|
||||
"name": "Bolliger & Mabillard",
|
||||
"slug": "bolliger-mabillard",
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class RideModelDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model details."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
|
||||
# Manufacturer info
|
||||
manufacturer = serializers.SerializerMethodField()
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||||
def get_manufacturer(self, obj) -> dict | None:
|
||||
if obj.manufacturer:
|
||||
return {
|
||||
"id": obj.manufacturer.id,
|
||||
"name": obj.manufacturer.name,
|
||||
"slug": obj.manufacturer.slug,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
class RideModelCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating ride models."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
category = serializers.ChoiceField(choices=CATEGORY_CHOICES, required=False)
|
||||
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class RideModelUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride models."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
category = serializers.ChoiceField(choices=CATEGORY_CHOICES, required=False)
|
||||
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
187
backend/apps/api/v1/serializers/history.py
Normal file
187
backend/apps/api/v1/serializers/history.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
History domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains serializers for history tracking and timeline functionality
|
||||
using django-pghistory.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema_serializer, extend_schema_field
|
||||
import pghistory.models
|
||||
|
||||
|
||||
class ParkHistoryEventSerializer(serializers.Serializer):
|
||||
"""Serializer for park history events."""
|
||||
|
||||
pgh_id = serializers.IntegerField(read_only=True)
|
||||
pgh_created_at = serializers.DateTimeField(read_only=True)
|
||||
pgh_label = serializers.CharField(read_only=True)
|
||||
pgh_obj_id = serializers.IntegerField(read_only=True)
|
||||
pgh_context = serializers.JSONField(read_only=True, allow_null=True)
|
||||
pgh_data = serializers.JSONField(read_only=True)
|
||||
event_type = serializers.SerializerMethodField()
|
||||
changes = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.CharField())
|
||||
def get_event_type(self, obj) -> str:
|
||||
"""Get human-readable event type."""
|
||||
return obj.pgh_label.replace("_", " ").title()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_changes(self, obj) -> dict:
|
||||
"""Get changes made in this event."""
|
||||
if hasattr(obj, "pgh_diff") and obj.pgh_diff:
|
||||
return obj.pgh_diff
|
||||
return {}
|
||||
|
||||
|
||||
class RideHistoryEventSerializer(serializers.Serializer):
|
||||
"""Serializer for ride history events."""
|
||||
|
||||
pgh_id = serializers.IntegerField(read_only=True)
|
||||
pgh_created_at = serializers.DateTimeField(read_only=True)
|
||||
pgh_label = serializers.CharField(read_only=True)
|
||||
pgh_obj_id = serializers.IntegerField(read_only=True)
|
||||
pgh_context = serializers.JSONField(read_only=True, allow_null=True)
|
||||
pgh_data = serializers.JSONField(read_only=True)
|
||||
event_type = serializers.SerializerMethodField()
|
||||
changes = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.CharField())
|
||||
def get_event_type(self, obj) -> str:
|
||||
"""Get human-readable event type."""
|
||||
return obj.pgh_label.replace("_", " ").title()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_changes(self, obj) -> dict:
|
||||
"""Get changes made in this event."""
|
||||
if hasattr(obj, "pgh_diff") and obj.pgh_diff:
|
||||
return obj.pgh_diff
|
||||
return {}
|
||||
|
||||
|
||||
class HistorySummarySerializer(serializers.Serializer):
|
||||
"""Serializer for history summary information."""
|
||||
|
||||
total_events = serializers.IntegerField()
|
||||
first_recorded = serializers.DateTimeField(allow_null=True)
|
||||
last_modified = serializers.DateTimeField(allow_null=True)
|
||||
|
||||
|
||||
class ParkHistoryOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for complete park history."""
|
||||
|
||||
park = serializers.SerializerMethodField()
|
||||
current_state = serializers.SerializerMethodField()
|
||||
summary = HistorySummarySerializer()
|
||||
events = ParkHistoryEventSerializer(many=True)
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_park(self, obj) -> dict:
|
||||
"""Get basic park information."""
|
||||
park = obj.get("park")
|
||||
if park:
|
||||
return {
|
||||
"id": park.id,
|
||||
"name": park.name,
|
||||
"slug": park.slug,
|
||||
"status": park.status,
|
||||
}
|
||||
return {}
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_current_state(self, obj) -> dict:
|
||||
"""Get current park state."""
|
||||
park = obj.get("current_state")
|
||||
if park:
|
||||
return {
|
||||
"id": park.id,
|
||||
"name": park.name,
|
||||
"slug": park.slug,
|
||||
"status": park.status,
|
||||
"opening_date": (
|
||||
park.opening_date.isoformat()
|
||||
if hasattr(park, "opening_date") and park.opening_date
|
||||
else None
|
||||
),
|
||||
"coaster_count": getattr(park, "coaster_count", 0),
|
||||
"ride_count": getattr(park, "ride_count", 0),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
class RideHistoryOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for complete ride history."""
|
||||
|
||||
ride = serializers.SerializerMethodField()
|
||||
current_state = serializers.SerializerMethodField()
|
||||
summary = HistorySummarySerializer()
|
||||
events = RideHistoryEventSerializer(many=True)
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_ride(self, obj) -> dict:
|
||||
"""Get basic ride information."""
|
||||
ride = obj.get("ride")
|
||||
if ride:
|
||||
return {
|
||||
"id": ride.id,
|
||||
"name": ride.name,
|
||||
"slug": ride.slug,
|
||||
"park_name": ride.park.name if hasattr(ride, "park") else None,
|
||||
"status": getattr(ride, "status", "UNKNOWN"),
|
||||
}
|
||||
return {}
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_current_state(self, obj) -> dict:
|
||||
"""Get current ride state."""
|
||||
ride = obj.get("current_state")
|
||||
if ride:
|
||||
return {
|
||||
"id": ride.id,
|
||||
"name": ride.name,
|
||||
"slug": ride.slug,
|
||||
"park_name": ride.park.name if hasattr(ride, "park") else None,
|
||||
"status": getattr(ride, "status", "UNKNOWN"),
|
||||
"opening_date": (
|
||||
ride.opening_date.isoformat()
|
||||
if hasattr(ride, "opening_date") and ride.opening_date
|
||||
else None
|
||||
),
|
||||
"ride_type": getattr(ride, "ride_type", "Unknown"),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
class UnifiedHistoryTimelineSerializer(serializers.Serializer):
|
||||
"""Serializer for unified history timeline."""
|
||||
|
||||
summary = serializers.SerializerMethodField()
|
||||
events = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_summary(self, obj) -> dict:
|
||||
"""Get timeline summary."""
|
||||
return obj.get("summary", {})
|
||||
|
||||
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
|
||||
def get_events(self, obj) -> list:
|
||||
"""Get timeline events."""
|
||||
events = obj.get("events", [])
|
||||
event_data = []
|
||||
|
||||
for event in events:
|
||||
event_data.append(
|
||||
{
|
||||
"pgh_id": event.pgh_id,
|
||||
"pgh_created_at": event.pgh_created_at,
|
||||
"pgh_label": event.pgh_label,
|
||||
"pgh_model": event.pgh_model,
|
||||
"pgh_obj_id": event.pgh_obj_id,
|
||||
"pgh_context": event.pgh_context,
|
||||
"event_type": event.pgh_label.replace("_", " ").title(),
|
||||
"model_type": event.pgh_model.split(".")[-1].title(),
|
||||
}
|
||||
)
|
||||
|
||||
return event_data
|
||||
124
backend/apps/api/v1/serializers/media.py
Normal file
124
backend/apps/api/v1/serializers/media.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Media domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains serializers for photo uploads, media management,
|
||||
and related media functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
|
||||
# === MEDIA SERIALIZERS ===
|
||||
|
||||
|
||||
class PhotoUploadInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for photo uploads."""
|
||||
|
||||
file = serializers.ImageField()
|
||||
caption = serializers.CharField(
|
||||
max_length=500,
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
help_text="Optional caption for the photo",
|
||||
)
|
||||
alt_text = serializers.CharField(
|
||||
max_length=255,
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
help_text="Alt text for accessibility",
|
||||
)
|
||||
is_primary = serializers.BooleanField(
|
||||
default=False, help_text="Whether this should be the primary photo"
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Photo Detail Example",
|
||||
summary="Example photo detail response",
|
||||
description="A photo with full details",
|
||||
value={
|
||||
"id": 1,
|
||||
"url": "https://example.com/media/photos/ride123.jpg",
|
||||
"thumbnail_url": "https://example.com/media/thumbnails/ride123_thumb.jpg",
|
||||
"caption": "Amazing view of Steel Vengeance",
|
||||
"alt_text": "Steel Vengeance roller coaster with blue sky",
|
||||
"is_primary": True,
|
||||
"uploaded_at": "2024-08-15T10:30:00Z",
|
||||
"uploaded_by": {
|
||||
"id": 1,
|
||||
"username": "coaster_photographer",
|
||||
"display_name": "Coaster Photographer",
|
||||
},
|
||||
"content_type": "Ride",
|
||||
"object_id": 123,
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class PhotoDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for photo details."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
url = serializers.URLField()
|
||||
thumbnail_url = serializers.URLField(required=False)
|
||||
caption = serializers.CharField()
|
||||
alt_text = serializers.CharField()
|
||||
is_primary = serializers.BooleanField()
|
||||
uploaded_at = serializers.DateTimeField()
|
||||
content_type = serializers.CharField()
|
||||
object_id = serializers.IntegerField()
|
||||
|
||||
# File metadata
|
||||
file_size = serializers.IntegerField()
|
||||
width = serializers.IntegerField()
|
||||
height = serializers.IntegerField()
|
||||
format = serializers.CharField()
|
||||
|
||||
# Uploader info
|
||||
uploaded_by = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_uploaded_by(self, obj) -> dict:
|
||||
"""Get uploader information."""
|
||||
return {
|
||||
"id": obj.uploaded_by.id,
|
||||
"username": obj.uploaded_by.username,
|
||||
"display_name": getattr(
|
||||
obj.uploaded_by, "get_display_name", lambda: obj.uploaded_by.username
|
||||
)(),
|
||||
}
|
||||
|
||||
|
||||
class PhotoListOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for photo list view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
url = serializers.URLField()
|
||||
thumbnail_url = serializers.URLField(required=False)
|
||||
caption = serializers.CharField()
|
||||
is_primary = serializers.BooleanField()
|
||||
uploaded_at = serializers.DateTimeField()
|
||||
uploaded_by = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_uploaded_by(self, obj) -> dict:
|
||||
"""Get uploader information."""
|
||||
return {
|
||||
"id": obj.uploaded_by.id,
|
||||
"username": obj.uploaded_by.username,
|
||||
}
|
||||
|
||||
|
||||
class PhotoUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating photos."""
|
||||
|
||||
caption = serializers.CharField(max_length=500, required=False, allow_blank=True)
|
||||
alt_text = serializers.CharField(max_length=255, required=False, allow_blank=True)
|
||||
is_primary = serializers.BooleanField(required=False)
|
||||
118
backend/apps/api/v1/serializers/other.py
Normal file
118
backend/apps/api/v1/serializers/other.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Statistics, health check, and miscellaneous domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains serializers for statistics, health checks, and other
|
||||
miscellaneous functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
|
||||
# === STATISTICS SERIALIZERS ===
|
||||
|
||||
|
||||
class ParkStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park statistics."""
|
||||
|
||||
total_parks = serializers.IntegerField()
|
||||
operating_parks = serializers.IntegerField()
|
||||
closed_parks = serializers.IntegerField()
|
||||
under_construction = serializers.IntegerField()
|
||||
|
||||
# Averages
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
average_coaster_count = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, allow_null=True
|
||||
)
|
||||
|
||||
# Top countries
|
||||
top_countries = serializers.ListField(child=serializers.DictField())
|
||||
|
||||
# Recently added
|
||||
recently_added_count = serializers.IntegerField()
|
||||
|
||||
|
||||
class RideStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride statistics."""
|
||||
|
||||
total_rides = serializers.IntegerField()
|
||||
operating_rides = serializers.IntegerField()
|
||||
closed_rides = serializers.IntegerField()
|
||||
under_construction = serializers.IntegerField()
|
||||
|
||||
# By category
|
||||
rides_by_category = serializers.DictField()
|
||||
|
||||
# Averages
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
average_capacity = serializers.DecimalField(
|
||||
max_digits=8, decimal_places=2, allow_null=True
|
||||
)
|
||||
|
||||
# Top manufacturers
|
||||
top_manufacturers = serializers.ListField(child=serializers.DictField())
|
||||
|
||||
# Recently added
|
||||
recently_added_count = serializers.IntegerField()
|
||||
|
||||
|
||||
class ParkReviewOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park reviews."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
rating = serializers.IntegerField()
|
||||
title = serializers.CharField()
|
||||
content = serializers.CharField()
|
||||
visit_date = serializers.DateField()
|
||||
created_at = serializers.DateTimeField()
|
||||
|
||||
# User info (limited for privacy)
|
||||
user = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_user(self, obj) -> dict:
|
||||
return {
|
||||
"username": obj.user.username,
|
||||
"display_name": obj.user.get_full_name() or obj.user.username,
|
||||
}
|
||||
|
||||
|
||||
# === HEALTH CHECK SERIALIZERS ===
|
||||
|
||||
|
||||
class HealthCheckOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for health check responses."""
|
||||
|
||||
status = serializers.ChoiceField(choices=["healthy", "unhealthy"])
|
||||
timestamp = serializers.DateTimeField()
|
||||
version = serializers.CharField()
|
||||
environment = serializers.CharField()
|
||||
response_time_ms = serializers.FloatField()
|
||||
checks = serializers.DictField()
|
||||
metrics = serializers.DictField()
|
||||
|
||||
|
||||
class PerformanceMetricsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for performance metrics."""
|
||||
|
||||
timestamp = serializers.DateTimeField()
|
||||
database_analysis = serializers.DictField()
|
||||
cache_performance = serializers.DictField()
|
||||
recent_slow_queries = serializers.ListField()
|
||||
|
||||
|
||||
class SimpleHealthOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for simple health check."""
|
||||
|
||||
status = serializers.ChoiceField(choices=["ok", "error"])
|
||||
timestamp = serializers.DateTimeField()
|
||||
error = serializers.CharField(required=False)
|
||||
448
backend/apps/api/v1/serializers/parks.py
Normal file
448
backend/apps/api/v1/serializers/parks.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""
|
||||
Parks domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains all serializers related to parks, park areas, park locations,
|
||||
and park search functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
from .shared import LocationOutputSerializer, CompanyOutputSerializer, ModelChoices
|
||||
|
||||
|
||||
# === PARK SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Park List Example",
|
||||
summary="Example park list response",
|
||||
description="A typical park in the list view",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Cedar Point",
|
||||
"slug": "cedar-point",
|
||||
"status": "OPERATING",
|
||||
"description": "America's Roller Coast",
|
||||
"average_rating": 4.5,
|
||||
"coaster_count": 17,
|
||||
"ride_count": 70,
|
||||
"location": {
|
||||
"city": "Sandusky",
|
||||
"state": "Ohio",
|
||||
"country": "United States",
|
||||
},
|
||||
"operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class ParkListOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park list view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
# Statistics
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
coaster_count = serializers.IntegerField(allow_null=True)
|
||||
ride_count = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Location (simplified for list view)
|
||||
location = LocationOutputSerializer(allow_null=True)
|
||||
|
||||
# Operator info
|
||||
operator = CompanyOutputSerializer()
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Park Detail Example",
|
||||
summary="Example park detail response",
|
||||
description="A complete park detail response",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Cedar Point",
|
||||
"slug": "cedar-point",
|
||||
"status": "OPERATING",
|
||||
"description": "America's Roller Coast",
|
||||
"opening_date": "1870-01-01",
|
||||
"website": "https://cedarpoint.com",
|
||||
"size_acres": 364.0,
|
||||
"average_rating": 4.5,
|
||||
"coaster_count": 17,
|
||||
"ride_count": 70,
|
||||
"location": {
|
||||
"latitude": 41.4793,
|
||||
"longitude": -82.6833,
|
||||
"city": "Sandusky",
|
||||
"state": "Ohio",
|
||||
"country": "United States",
|
||||
},
|
||||
"operator": {"id": 1, "name": "Cedar Fair", "slug": "cedar-fair"},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class ParkDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park detail view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
# Details
|
||||
opening_date = serializers.DateField(allow_null=True)
|
||||
closing_date = serializers.DateField(allow_null=True)
|
||||
operating_season = serializers.CharField()
|
||||
size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, allow_null=True
|
||||
)
|
||||
website = serializers.URLField()
|
||||
|
||||
# Statistics
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
coaster_count = serializers.IntegerField(allow_null=True)
|
||||
ride_count = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Location (full details)
|
||||
location = LocationOutputSerializer(allow_null=True)
|
||||
|
||||
# Companies
|
||||
operator = CompanyOutputSerializer()
|
||||
property_owner = CompanyOutputSerializer(allow_null=True)
|
||||
|
||||
# Areas
|
||||
areas = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
|
||||
def get_areas(self, obj):
|
||||
"""Get simplified area information."""
|
||||
if hasattr(obj, "areas"):
|
||||
return [
|
||||
{
|
||||
"id": area.id,
|
||||
"name": area.name,
|
||||
"slug": area.slug,
|
||||
"description": area.description,
|
||||
}
|
||||
for area in obj.areas.all()
|
||||
]
|
||||
return []
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
class ParkCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating parks."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
status = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_park_status_choices(), default="OPERATING"
|
||||
)
|
||||
|
||||
# Optional details
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
operating_season = serializers.CharField(
|
||||
max_length=255, required=False, allow_blank=True
|
||||
)
|
||||
size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
website = serializers.URLField(required=False, allow_blank=True)
|
||||
|
||||
# Required operator
|
||||
operator_id = serializers.IntegerField()
|
||||
|
||||
# Optional property owner
|
||||
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
opening_date = attrs.get("opening_date")
|
||||
closing_date = attrs.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class ParkUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating parks."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
status = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_park_status_choices(), required=False
|
||||
)
|
||||
|
||||
# Optional details
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
operating_season = serializers.CharField(
|
||||
max_length=255, required=False, allow_blank=True
|
||||
)
|
||||
size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
website = serializers.URLField(required=False, allow_blank=True)
|
||||
|
||||
# Companies
|
||||
operator_id = serializers.IntegerField(required=False)
|
||||
property_owner_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
opening_date = attrs.get("opening_date")
|
||||
closing_date = attrs.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class ParkFilterInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for park filtering and search."""
|
||||
|
||||
# Search
|
||||
search = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Status filter
|
||||
status = serializers.MultipleChoiceField(
|
||||
choices=[], required=False # Choices set dynamically
|
||||
)
|
||||
|
||||
# Location filters
|
||||
country = serializers.CharField(required=False, allow_blank=True)
|
||||
state = serializers.CharField(required=False, allow_blank=True)
|
||||
city = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Rating filter
|
||||
min_rating = serializers.DecimalField(
|
||||
max_digits=3,
|
||||
decimal_places=2,
|
||||
required=False,
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
)
|
||||
|
||||
# Size filter
|
||||
min_size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, min_value=0
|
||||
)
|
||||
max_size_acres = serializers.DecimalField(
|
||||
max_digits=10, decimal_places=2, required=False, min_value=0
|
||||
)
|
||||
|
||||
# Company filters
|
||||
operator_id = serializers.IntegerField(required=False)
|
||||
property_owner_id = serializers.IntegerField(required=False)
|
||||
|
||||
# Ordering
|
||||
ordering = serializers.ChoiceField(
|
||||
choices=[
|
||||
"name",
|
||||
"-name",
|
||||
"opening_date",
|
||||
"-opening_date",
|
||||
"average_rating",
|
||||
"-average_rating",
|
||||
"coaster_count",
|
||||
"-coaster_count",
|
||||
"created_at",
|
||||
"-created_at",
|
||||
],
|
||||
required=False,
|
||||
default="name",
|
||||
)
|
||||
|
||||
|
||||
# === PARK AREA SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Park Area Example",
|
||||
summary="Example park area response",
|
||||
description="A themed area within a park",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Tomorrowland",
|
||||
"slug": "tomorrowland",
|
||||
"description": "A futuristic themed area",
|
||||
"park": {"id": 1, "name": "Magic Kingdom", "slug": "magic-kingdom"},
|
||||
"opening_date": "1971-10-01",
|
||||
"closing_date": None,
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class ParkAreaDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park areas."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
opening_date = serializers.DateField(allow_null=True)
|
||||
closing_date = serializers.DateField(allow_null=True)
|
||||
|
||||
# Park info
|
||||
park = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_park(self, obj) -> dict:
|
||||
return {
|
||||
"id": obj.park.id,
|
||||
"name": obj.park.name,
|
||||
"slug": obj.park.slug,
|
||||
}
|
||||
|
||||
|
||||
class ParkAreaCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating park areas."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
park_id = serializers.IntegerField()
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
opening_date = attrs.get("opening_date")
|
||||
closing_date = attrs.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class ParkAreaUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating park areas."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
opening_date = attrs.get("opening_date")
|
||||
closing_date = attrs.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
# === PARK LOCATION SERIALIZERS ===
|
||||
|
||||
|
||||
class ParkLocationOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park locations."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
latitude = serializers.FloatField(allow_null=True)
|
||||
longitude = serializers.FloatField(allow_null=True)
|
||||
address = serializers.CharField()
|
||||
city = serializers.CharField()
|
||||
state = serializers.CharField()
|
||||
country = serializers.CharField()
|
||||
postal_code = serializers.CharField()
|
||||
formatted_address = serializers.CharField()
|
||||
|
||||
# Park info
|
||||
park = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_park(self, obj) -> dict:
|
||||
return {
|
||||
"id": obj.park.id,
|
||||
"name": obj.park.name,
|
||||
"slug": obj.park.slug,
|
||||
}
|
||||
|
||||
|
||||
class ParkLocationCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating park locations."""
|
||||
|
||||
park_id = serializers.IntegerField()
|
||||
latitude = serializers.FloatField(required=False, allow_null=True)
|
||||
longitude = serializers.FloatField(required=False, allow_null=True)
|
||||
address = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
city = serializers.CharField(max_length=100)
|
||||
state = serializers.CharField(max_length=100)
|
||||
country = serializers.CharField(max_length=100)
|
||||
postal_code = serializers.CharField(max_length=20, allow_blank=True, default="")
|
||||
|
||||
|
||||
class ParkLocationUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating park locations."""
|
||||
|
||||
latitude = serializers.FloatField(required=False, allow_null=True)
|
||||
longitude = serializers.FloatField(required=False, allow_null=True)
|
||||
address = serializers.CharField(max_length=255, allow_blank=True, required=False)
|
||||
city = serializers.CharField(max_length=100, required=False)
|
||||
state = serializers.CharField(max_length=100, required=False)
|
||||
country = serializers.CharField(max_length=100, required=False)
|
||||
postal_code = serializers.CharField(max_length=20, allow_blank=True, required=False)
|
||||
|
||||
|
||||
# === PARKS SEARCH SERIALIZERS ===
|
||||
|
||||
|
||||
class ParkSuggestionSerializer(serializers.Serializer):
|
||||
"""Serializer for park search suggestions."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
location = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
coaster_count = serializers.IntegerField()
|
||||
|
||||
|
||||
class ParkSuggestionOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park suggestions."""
|
||||
|
||||
results = ParkSuggestionSerializer(many=True)
|
||||
query = serializers.CharField()
|
||||
count = serializers.IntegerField()
|
||||
116
backend/apps/api/v1/serializers/parks_media.py
Normal file
116
backend/apps/api/v1/serializers/parks_media.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Park media serializers for ThrillWiki API.
|
||||
|
||||
This module contains serializers for park-specific media functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from apps.parks.models import ParkPhoto
|
||||
|
||||
|
||||
class ParkPhotoOutputSerializer(serializers.ModelSerializer):
|
||||
"""Output serializer for park photos."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source='uploaded_by.username', read_only=True)
|
||||
file_size = serializers.ReadOnlyField()
|
||||
dimensions = serializers.ReadOnlyField()
|
||||
park_slug = serializers.CharField(source='park.slug', read_only=True)
|
||||
park_name = serializers.CharField(source='park.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ParkPhoto
|
||||
fields = [
|
||||
'id',
|
||||
'image',
|
||||
'caption',
|
||||
'alt_text',
|
||||
'is_primary',
|
||||
'is_approved',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'date_taken',
|
||||
'uploaded_by_username',
|
||||
'file_size',
|
||||
'dimensions',
|
||||
'park_slug',
|
||||
'park_name',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'uploaded_by_username',
|
||||
'file_size',
|
||||
'dimensions',
|
||||
'park_slug',
|
||||
'park_name',
|
||||
]
|
||||
|
||||
|
||||
class ParkPhotoCreateInputSerializer(serializers.ModelSerializer):
|
||||
"""Input serializer for creating park photos."""
|
||||
|
||||
class Meta:
|
||||
model = ParkPhoto
|
||||
fields = [
|
||||
'image',
|
||||
'caption',
|
||||
'alt_text',
|
||||
'is_primary',
|
||||
]
|
||||
|
||||
|
||||
class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer):
|
||||
"""Input serializer for updating park photos."""
|
||||
|
||||
class Meta:
|
||||
model = ParkPhoto
|
||||
fields = [
|
||||
'caption',
|
||||
'alt_text',
|
||||
'is_primary',
|
||||
]
|
||||
|
||||
|
||||
class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
|
||||
"""Simplified output serializer for park photo lists."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source='uploaded_by.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ParkPhoto
|
||||
fields = [
|
||||
'id',
|
||||
'image',
|
||||
'caption',
|
||||
'is_primary',
|
||||
'is_approved',
|
||||
'created_at',
|
||||
'uploaded_by_username',
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ParkPhotoApprovalInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for photo approval operations."""
|
||||
|
||||
photo_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="List of photo IDs to approve"
|
||||
)
|
||||
approve = serializers.BooleanField(
|
||||
default=True,
|
||||
help_text="Whether to approve (True) or reject (False) the photos"
|
||||
)
|
||||
|
||||
|
||||
class ParkPhotoStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for park photo statistics."""
|
||||
|
||||
total_photos = serializers.IntegerField()
|
||||
approved_photos = serializers.IntegerField()
|
||||
pending_photos = serializers.IntegerField()
|
||||
has_primary = serializers.BooleanField()
|
||||
recent_uploads = serializers.IntegerField()
|
||||
651
backend/apps/api/v1/serializers/rides.py
Normal file
651
backend/apps/api/v1/serializers/rides.py
Normal file
@@ -0,0 +1,651 @@
|
||||
"""
|
||||
Rides domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains all serializers related to rides, roller coaster statistics,
|
||||
ride locations, and ride reviews.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
from .shared import ModelChoices
|
||||
|
||||
|
||||
# === RIDE SERIALIZERS ===
|
||||
|
||||
|
||||
class RideParkOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride's park data."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
|
||||
|
||||
class RideModelOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride model data."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
manufacturer = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||||
def get_manufacturer(self, obj) -> dict | None:
|
||||
if obj.manufacturer:
|
||||
return {
|
||||
"id": obj.manufacturer.id,
|
||||
"name": obj.manufacturer.name,
|
||||
"slug": obj.manufacturer.slug,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Ride List Example",
|
||||
summary="Example ride list response",
|
||||
description="A typical ride in the list view",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Steel Vengeance",
|
||||
"slug": "steel-vengeance",
|
||||
"category": "ROLLER_COASTER",
|
||||
"status": "OPERATING",
|
||||
"description": "Hybrid roller coaster",
|
||||
"park": {"id": 1, "name": "Cedar Point", "slug": "cedar-point"},
|
||||
"average_rating": 4.8,
|
||||
"capacity_per_hour": 1200,
|
||||
"opening_date": "2018-05-05",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class RideListOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride list view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
# Park info
|
||||
park = RideParkOutputSerializer()
|
||||
|
||||
# Statistics
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
capacity_per_hour = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Dates
|
||||
opening_date = serializers.DateField(allow_null=True)
|
||||
closing_date = serializers.DateField(allow_null=True)
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Ride Detail Example",
|
||||
summary="Example ride detail response",
|
||||
description="A complete ride detail response",
|
||||
value={
|
||||
"id": 1,
|
||||
"name": "Steel Vengeance",
|
||||
"slug": "steel-vengeance",
|
||||
"category": "ROLLER_COASTER",
|
||||
"status": "OPERATING",
|
||||
"description": "Hybrid roller coaster featuring RMC I-Box track",
|
||||
"park": {"id": 1, "name": "Cedar Point", "slug": "cedar-point"},
|
||||
"opening_date": "2018-05-05",
|
||||
"min_height_in": 48,
|
||||
"capacity_per_hour": 1200,
|
||||
"ride_duration_seconds": 150,
|
||||
"average_rating": 4.8,
|
||||
"manufacturer": {
|
||||
"id": 1,
|
||||
"name": "Rocky Mountain Construction",
|
||||
"slug": "rocky-mountain-construction",
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class RideDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride detail view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
post_closing_status = serializers.CharField(allow_null=True)
|
||||
description = serializers.CharField()
|
||||
|
||||
# Park info
|
||||
park = RideParkOutputSerializer()
|
||||
park_area = serializers.SerializerMethodField()
|
||||
|
||||
# Dates
|
||||
opening_date = serializers.DateField(allow_null=True)
|
||||
closing_date = serializers.DateField(allow_null=True)
|
||||
status_since = serializers.DateField(allow_null=True)
|
||||
|
||||
# Physical specs
|
||||
min_height_in = serializers.IntegerField(allow_null=True)
|
||||
max_height_in = serializers.IntegerField(allow_null=True)
|
||||
capacity_per_hour = serializers.IntegerField(allow_null=True)
|
||||
ride_duration_seconds = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Statistics
|
||||
average_rating = serializers.DecimalField(
|
||||
max_digits=3, decimal_places=2, allow_null=True
|
||||
)
|
||||
|
||||
# Companies
|
||||
manufacturer = serializers.SerializerMethodField()
|
||||
designer = serializers.SerializerMethodField()
|
||||
|
||||
# Model
|
||||
ride_model = RideModelOutputSerializer(allow_null=True)
|
||||
|
||||
# Metadata
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||||
def get_park_area(self, obj) -> dict | None:
|
||||
if obj.park_area:
|
||||
return {
|
||||
"id": obj.park_area.id,
|
||||
"name": obj.park_area.name,
|
||||
"slug": obj.park_area.slug,
|
||||
}
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||||
def get_manufacturer(self, obj) -> dict | None:
|
||||
if obj.manufacturer:
|
||||
return {
|
||||
"id": obj.manufacturer.id,
|
||||
"name": obj.manufacturer.name,
|
||||
"slug": obj.manufacturer.slug,
|
||||
}
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||||
def get_designer(self, obj) -> dict | None:
|
||||
if obj.designer:
|
||||
return {
|
||||
"id": obj.designer.id,
|
||||
"name": obj.designer.name,
|
||||
"slug": obj.designer.slug,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
class RideCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating rides."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
category = serializers.ChoiceField(choices=[]) # Choices set dynamically
|
||||
status = serializers.ChoiceField(
|
||||
choices=[], default="OPERATING"
|
||||
) # Choices set dynamically
|
||||
|
||||
# Required park
|
||||
park_id = serializers.IntegerField()
|
||||
|
||||
# Optional area
|
||||
park_area_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
# Optional dates
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
status_since = serializers.DateField(required=False, allow_null=True)
|
||||
|
||||
# Optional specs
|
||||
min_height_in = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=30, max_value=90
|
||||
)
|
||||
max_height_in = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=30, max_value=90
|
||||
)
|
||||
capacity_per_hour = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
ride_duration_seconds = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
|
||||
# Optional companies
|
||||
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
designer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
# Optional model
|
||||
ride_model_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
# Date validation
|
||||
opening_date = attrs.get("opening_date")
|
||||
closing_date = attrs.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
# Height validation
|
||||
min_height = attrs.get("min_height_in")
|
||||
max_height = attrs.get("max_height_in")
|
||||
|
||||
if min_height and max_height and min_height > max_height:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum height cannot be greater than maximum height"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class RideUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating rides."""
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
category = serializers.ChoiceField(
|
||||
choices=[], required=False
|
||||
) # Choices set dynamically
|
||||
status = serializers.ChoiceField(
|
||||
choices=[], required=False
|
||||
) # Choices set dynamically
|
||||
post_closing_status = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_ride_post_closing_choices(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
# Park and area
|
||||
park_id = serializers.IntegerField(required=False)
|
||||
park_area_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
# Dates
|
||||
opening_date = serializers.DateField(required=False, allow_null=True)
|
||||
closing_date = serializers.DateField(required=False, allow_null=True)
|
||||
status_since = serializers.DateField(required=False, allow_null=True)
|
||||
|
||||
# Specs
|
||||
min_height_in = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=30, max_value=90
|
||||
)
|
||||
max_height_in = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=30, max_value=90
|
||||
)
|
||||
capacity_per_hour = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
ride_duration_seconds = serializers.IntegerField(
|
||||
required=False, allow_null=True, min_value=1
|
||||
)
|
||||
|
||||
# Companies
|
||||
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
designer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
# Model
|
||||
ride_model_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Cross-field validation."""
|
||||
# Date validation
|
||||
opening_date = attrs.get("opening_date")
|
||||
closing_date = attrs.get("closing_date")
|
||||
|
||||
if opening_date and closing_date and closing_date < opening_date:
|
||||
raise serializers.ValidationError(
|
||||
"Closing date cannot be before opening date"
|
||||
)
|
||||
|
||||
# Height validation
|
||||
min_height = attrs.get("min_height_in")
|
||||
max_height = attrs.get("max_height_in")
|
||||
|
||||
if min_height and max_height and min_height > max_height:
|
||||
raise serializers.ValidationError(
|
||||
"Minimum height cannot be greater than maximum height"
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class RideFilterInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for ride filtering and search."""
|
||||
|
||||
# Search
|
||||
search = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Category filter
|
||||
category = serializers.MultipleChoiceField(
|
||||
choices=[], required=False
|
||||
) # Choices set dynamically
|
||||
|
||||
# Status filter
|
||||
status = serializers.MultipleChoiceField(
|
||||
choices=[], required=False # Choices set dynamically
|
||||
)
|
||||
|
||||
# Park filter
|
||||
park_id = serializers.IntegerField(required=False)
|
||||
park_slug = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
# Company filters
|
||||
manufacturer_id = serializers.IntegerField(required=False)
|
||||
designer_id = serializers.IntegerField(required=False)
|
||||
|
||||
# Rating filter
|
||||
min_rating = serializers.DecimalField(
|
||||
max_digits=3,
|
||||
decimal_places=2,
|
||||
required=False,
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
)
|
||||
|
||||
# Height filters
|
||||
min_height_requirement = serializers.IntegerField(required=False)
|
||||
max_height_requirement = serializers.IntegerField(required=False)
|
||||
|
||||
# Capacity filter
|
||||
min_capacity = serializers.IntegerField(required=False)
|
||||
|
||||
# Ordering
|
||||
ordering = serializers.ChoiceField(
|
||||
choices=[
|
||||
"name",
|
||||
"-name",
|
||||
"opening_date",
|
||||
"-opening_date",
|
||||
"average_rating",
|
||||
"-average_rating",
|
||||
"capacity_per_hour",
|
||||
"-capacity_per_hour",
|
||||
"created_at",
|
||||
"-created_at",
|
||||
],
|
||||
required=False,
|
||||
default="name",
|
||||
)
|
||||
|
||||
|
||||
# === ROLLER COASTER STATS SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Roller Coaster Stats Example",
|
||||
summary="Example roller coaster statistics",
|
||||
description="Detailed statistics for a roller coaster",
|
||||
value={
|
||||
"id": 1,
|
||||
"ride": {"id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance"},
|
||||
"height_ft": 205.0,
|
||||
"length_ft": 5740.0,
|
||||
"speed_mph": 74.0,
|
||||
"inversions": 4,
|
||||
"ride_time_seconds": 150,
|
||||
"track_material": "HYBRID",
|
||||
"roller_coaster_type": "SITDOWN",
|
||||
"launch_type": "CHAIN",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class RollerCoasterStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for roller coaster statistics."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, allow_null=True
|
||||
)
|
||||
length_ft = serializers.DecimalField(
|
||||
max_digits=7, decimal_places=2, allow_null=True
|
||||
)
|
||||
speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, allow_null=True
|
||||
)
|
||||
inversions = serializers.IntegerField()
|
||||
ride_time_seconds = serializers.IntegerField(allow_null=True)
|
||||
track_type = serializers.CharField()
|
||||
track_material = serializers.CharField()
|
||||
roller_coaster_type = serializers.CharField()
|
||||
max_drop_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, allow_null=True
|
||||
)
|
||||
launch_type = serializers.CharField()
|
||||
train_style = serializers.CharField()
|
||||
trains_count = serializers.IntegerField(allow_null=True)
|
||||
cars_per_train = serializers.IntegerField(allow_null=True)
|
||||
seats_per_car = serializers.IntegerField(allow_null=True)
|
||||
|
||||
# Ride info
|
||||
ride = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_ride(self, obj) -> dict:
|
||||
return {
|
||||
"id": obj.ride.id,
|
||||
"name": obj.ride.name,
|
||||
"slug": obj.ride.slug,
|
||||
}
|
||||
|
||||
|
||||
class RollerCoasterStatsCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating roller coaster statistics."""
|
||||
|
||||
ride_id = serializers.IntegerField()
|
||||
height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
length_ft = serializers.DecimalField(
|
||||
max_digits=7, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
inversions = serializers.IntegerField(default=0)
|
||||
ride_time_seconds = serializers.IntegerField(required=False, allow_null=True)
|
||||
track_type = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
track_material = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_coaster_track_choices(), default="STEEL"
|
||||
)
|
||||
roller_coaster_type = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_coaster_type_choices(), default="SITDOWN"
|
||||
)
|
||||
max_drop_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
launch_type = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_launch_choices(), default="CHAIN"
|
||||
)
|
||||
train_style = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
trains_count = serializers.IntegerField(required=False, allow_null=True)
|
||||
cars_per_train = serializers.IntegerField(required=False, allow_null=True)
|
||||
seats_per_car = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class RollerCoasterStatsUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating roller coaster statistics."""
|
||||
|
||||
height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
length_ft = serializers.DecimalField(
|
||||
max_digits=7, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
speed_mph = serializers.DecimalField(
|
||||
max_digits=5, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
inversions = serializers.IntegerField(required=False)
|
||||
ride_time_seconds = serializers.IntegerField(required=False, allow_null=True)
|
||||
track_type = serializers.CharField(max_length=255, allow_blank=True, required=False)
|
||||
track_material = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_coaster_track_choices(), required=False
|
||||
)
|
||||
roller_coaster_type = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_coaster_type_choices(), required=False
|
||||
)
|
||||
max_drop_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, required=False, allow_null=True
|
||||
)
|
||||
launch_type = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_launch_choices(), required=False
|
||||
)
|
||||
train_style = serializers.CharField(
|
||||
max_length=255, allow_blank=True, required=False
|
||||
)
|
||||
trains_count = serializers.IntegerField(required=False, allow_null=True)
|
||||
cars_per_train = serializers.IntegerField(required=False, allow_null=True)
|
||||
seats_per_car = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
|
||||
# === RIDE LOCATION SERIALIZERS ===
|
||||
|
||||
|
||||
class RideLocationOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride locations."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
latitude = serializers.FloatField(allow_null=True)
|
||||
longitude = serializers.FloatField(allow_null=True)
|
||||
coordinates = serializers.CharField()
|
||||
|
||||
# Ride info
|
||||
ride = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_ride(self, obj) -> dict:
|
||||
return {
|
||||
"id": obj.ride.id,
|
||||
"name": obj.ride.name,
|
||||
"slug": obj.ride.slug,
|
||||
}
|
||||
|
||||
|
||||
class RideLocationCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating ride locations."""
|
||||
|
||||
ride_id = serializers.IntegerField()
|
||||
latitude = serializers.FloatField(required=False, allow_null=True)
|
||||
longitude = serializers.FloatField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class RideLocationUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride locations."""
|
||||
|
||||
latitude = serializers.FloatField(required=False, allow_null=True)
|
||||
longitude = serializers.FloatField(required=False, allow_null=True)
|
||||
|
||||
|
||||
# === RIDE REVIEW SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Ride Review Example",
|
||||
summary="Example ride review response",
|
||||
description="A user review of a ride",
|
||||
value={
|
||||
"id": 1,
|
||||
"rating": 9,
|
||||
"title": "Amazing coaster!",
|
||||
"content": "This ride was incredible, the airtime was fantastic.",
|
||||
"visit_date": "2024-08-15",
|
||||
"ride": {"id": 1, "name": "Steel Vengeance", "slug": "steel-vengeance"},
|
||||
"user": {"username": "coaster_fan", "display_name": "Coaster Fan"},
|
||||
"created_at": "2024-08-16T10:30:00Z",
|
||||
"is_published": True,
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class RideReviewOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride reviews."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
rating = serializers.IntegerField()
|
||||
title = serializers.CharField()
|
||||
content = serializers.CharField()
|
||||
visit_date = serializers.DateField()
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
is_published = serializers.BooleanField()
|
||||
|
||||
# Ride info
|
||||
ride = serializers.SerializerMethodField()
|
||||
# User info (limited for privacy)
|
||||
user = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_ride(self, obj) -> dict:
|
||||
return {
|
||||
"id": obj.ride.id,
|
||||
"name": obj.ride.name,
|
||||
"slug": obj.ride.slug,
|
||||
}
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_user(self, obj) -> dict:
|
||||
return {
|
||||
"username": obj.user.username,
|
||||
"display_name": obj.user.get_display_name(),
|
||||
}
|
||||
|
||||
|
||||
class RideReviewCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating ride reviews."""
|
||||
|
||||
ride_id = serializers.IntegerField()
|
||||
rating = serializers.IntegerField(min_value=1, max_value=10)
|
||||
title = serializers.CharField(max_length=200)
|
||||
content = serializers.CharField()
|
||||
visit_date = serializers.DateField()
|
||||
|
||||
def validate_visit_date(self, value):
|
||||
"""Validate visit date is not in the future."""
|
||||
from django.utils import timezone
|
||||
|
||||
if value > timezone.now().date():
|
||||
raise serializers.ValidationError("Visit date cannot be in the future")
|
||||
return value
|
||||
|
||||
|
||||
class RideReviewUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride reviews."""
|
||||
|
||||
rating = serializers.IntegerField(min_value=1, max_value=10, required=False)
|
||||
title = serializers.CharField(max_length=200, required=False)
|
||||
content = serializers.CharField(required=False)
|
||||
visit_date = serializers.DateField(required=False)
|
||||
|
||||
def validate_visit_date(self, value):
|
||||
"""Validate visit date is not in the future."""
|
||||
from django.utils import timezone
|
||||
|
||||
if value and value > timezone.now().date():
|
||||
raise serializers.ValidationError("Visit date cannot be in the future")
|
||||
return value
|
||||
147
backend/apps/api/v1/serializers/rides_media.py
Normal file
147
backend/apps/api/v1/serializers/rides_media.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Ride media serializers for ThrillWiki API.
|
||||
|
||||
This module contains serializers for ride-specific media functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from apps.rides.models import RidePhoto
|
||||
|
||||
|
||||
class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
||||
"""Output serializer for ride photos."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source='uploaded_by.username', read_only=True)
|
||||
file_size = serializers.ReadOnlyField()
|
||||
dimensions = serializers.ReadOnlyField()
|
||||
ride_slug = serializers.CharField(source='ride.slug', read_only=True)
|
||||
ride_name = serializers.CharField(source='ride.name', read_only=True)
|
||||
park_slug = serializers.CharField(source='ride.park.slug', read_only=True)
|
||||
park_name = serializers.CharField(source='ride.park.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RidePhoto
|
||||
fields = [
|
||||
'id',
|
||||
'image',
|
||||
'caption',
|
||||
'alt_text',
|
||||
'is_primary',
|
||||
'is_approved',
|
||||
'photo_type',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'date_taken',
|
||||
'uploaded_by_username',
|
||||
'file_size',
|
||||
'dimensions',
|
||||
'ride_slug',
|
||||
'ride_name',
|
||||
'park_slug',
|
||||
'park_name',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'uploaded_by_username',
|
||||
'file_size',
|
||||
'dimensions',
|
||||
'ride_slug',
|
||||
'ride_name',
|
||||
'park_slug',
|
||||
'park_name',
|
||||
]
|
||||
|
||||
|
||||
class RidePhotoCreateInputSerializer(serializers.ModelSerializer):
|
||||
"""Input serializer for creating ride photos."""
|
||||
|
||||
class Meta:
|
||||
model = RidePhoto
|
||||
fields = [
|
||||
'image',
|
||||
'caption',
|
||||
'alt_text',
|
||||
'photo_type',
|
||||
'is_primary',
|
||||
]
|
||||
|
||||
|
||||
class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
|
||||
"""Input serializer for updating ride photos."""
|
||||
|
||||
class Meta:
|
||||
model = RidePhoto
|
||||
fields = [
|
||||
'caption',
|
||||
'alt_text',
|
||||
'photo_type',
|
||||
'is_primary',
|
||||
]
|
||||
|
||||
|
||||
class RidePhotoListOutputSerializer(serializers.ModelSerializer):
|
||||
"""Simplified output serializer for ride photo lists."""
|
||||
|
||||
uploaded_by_username = serializers.CharField(
|
||||
source='uploaded_by.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RidePhoto
|
||||
fields = [
|
||||
'id',
|
||||
'image',
|
||||
'caption',
|
||||
'photo_type',
|
||||
'is_primary',
|
||||
'is_approved',
|
||||
'created_at',
|
||||
'uploaded_by_username',
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class RidePhotoApprovalInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for photo approval operations."""
|
||||
|
||||
photo_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="List of photo IDs to approve"
|
||||
)
|
||||
approve = serializers.BooleanField(
|
||||
default=True,
|
||||
help_text="Whether to approve (True) or reject (False) the photos"
|
||||
)
|
||||
|
||||
|
||||
class RidePhotoStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for ride photo statistics."""
|
||||
|
||||
total_photos = serializers.IntegerField()
|
||||
approved_photos = serializers.IntegerField()
|
||||
pending_photos = serializers.IntegerField()
|
||||
has_primary = serializers.BooleanField()
|
||||
recent_uploads = serializers.IntegerField()
|
||||
by_type = serializers.DictField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="Photo counts by type"
|
||||
)
|
||||
|
||||
|
||||
class RidePhotoTypeFilterSerializer(serializers.Serializer):
|
||||
"""Serializer for filtering photos by type."""
|
||||
|
||||
photo_type = serializers.ChoiceField(
|
||||
choices=[
|
||||
('exterior', 'Exterior View'),
|
||||
('queue', 'Queue Area'),
|
||||
('station', 'Station'),
|
||||
('onride', 'On-Ride'),
|
||||
('construction', 'Construction'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
required=False,
|
||||
help_text="Filter photos by type"
|
||||
)
|
||||
88
backend/apps/api/v1/serializers/search.py
Normal file
88
backend/apps/api/v1/serializers/search.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Search domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains serializers for entity search, location search,
|
||||
and other search functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
|
||||
# === CORE ENTITY SEARCH SERIALIZERS ===
|
||||
|
||||
|
||||
class EntitySearchInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for entity search requests."""
|
||||
|
||||
query = serializers.CharField(max_length=255, help_text="Search query string")
|
||||
entity_types = serializers.ListField(
|
||||
child=serializers.ChoiceField(choices=["park", "ride", "company", "user"]),
|
||||
required=False,
|
||||
help_text="Types of entities to search for",
|
||||
)
|
||||
limit = serializers.IntegerField(
|
||||
default=10,
|
||||
min_value=1,
|
||||
max_value=50,
|
||||
help_text="Maximum number of results to return",
|
||||
)
|
||||
|
||||
|
||||
class EntitySearchResultSerializer(serializers.Serializer):
|
||||
"""Serializer for individual entity search results."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
type = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
relevance_score = serializers.FloatField()
|
||||
|
||||
# Context-specific info
|
||||
context = serializers.JSONField(help_text="Additional context based on entity type")
|
||||
|
||||
|
||||
class EntitySearchOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for entity search results."""
|
||||
|
||||
query = serializers.CharField()
|
||||
total_results = serializers.IntegerField()
|
||||
results = EntitySearchResultSerializer(many=True)
|
||||
search_time_ms = serializers.FloatField()
|
||||
|
||||
|
||||
# === LOCATION SEARCH SERIALIZERS ===
|
||||
|
||||
|
||||
class LocationSearchResultSerializer(serializers.Serializer):
|
||||
"""Serializer for location search results."""
|
||||
|
||||
display_name = serializers.CharField()
|
||||
lat = serializers.FloatField()
|
||||
lon = serializers.FloatField()
|
||||
type = serializers.CharField()
|
||||
importance = serializers.FloatField()
|
||||
address = serializers.JSONField()
|
||||
|
||||
|
||||
class LocationSearchOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for location search."""
|
||||
|
||||
results = LocationSearchResultSerializer(many=True)
|
||||
query = serializers.CharField()
|
||||
count = serializers.IntegerField()
|
||||
|
||||
|
||||
class ReverseGeocodeOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for reverse geocoding."""
|
||||
|
||||
display_name = serializers.CharField()
|
||||
lat = serializers.FloatField()
|
||||
lon = serializers.FloatField()
|
||||
address = serializers.JSONField()
|
||||
type = serializers.CharField()
|
||||
229
backend/apps/api/v1/serializers/services.py
Normal file
229
backend/apps/api/v1/serializers/services.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Services domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains serializers for various services like email, maps,
|
||||
history tracking, moderation, and roadtrip planning.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
|
||||
# === EMAIL SERVICE SERIALIZERS ===
|
||||
|
||||
|
||||
class EmailSendInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for sending emails."""
|
||||
|
||||
to = serializers.EmailField()
|
||||
subject = serializers.CharField(max_length=255)
|
||||
text = serializers.CharField()
|
||||
html = serializers.CharField(required=False)
|
||||
template = serializers.CharField(required=False)
|
||||
context = serializers.JSONField(required=False)
|
||||
|
||||
|
||||
class EmailTemplateOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for email templates."""
|
||||
|
||||
id = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
subject = serializers.CharField()
|
||||
text_template = serializers.CharField()
|
||||
html_template = serializers.CharField(required=False)
|
||||
|
||||
|
||||
# === MAP SERVICE SERIALIZERS ===
|
||||
|
||||
|
||||
class MapDataOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for map data."""
|
||||
|
||||
parks = serializers.ListField(child=serializers.DictField())
|
||||
rides = serializers.ListField(child=serializers.DictField())
|
||||
bounds = serializers.DictField()
|
||||
zoom_level = serializers.IntegerField()
|
||||
|
||||
|
||||
class CoordinateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for coordinate-based requests."""
|
||||
|
||||
latitude = serializers.FloatField(min_value=-90, max_value=90)
|
||||
longitude = serializers.FloatField(min_value=-180, max_value=180)
|
||||
radius_km = serializers.FloatField(min_value=0, max_value=1000, default=10)
|
||||
|
||||
|
||||
# === HISTORY SERIALIZERS ===
|
||||
|
||||
|
||||
class HistoryEventSerializer(serializers.Serializer):
|
||||
"""Base serializer for history events from pghistory."""
|
||||
|
||||
pgh_id = serializers.IntegerField(read_only=True)
|
||||
pgh_created_at = serializers.DateTimeField(read_only=True)
|
||||
pgh_label = serializers.CharField(read_only=True)
|
||||
pgh_obj_id = serializers.IntegerField(read_only=True)
|
||||
pgh_context = serializers.JSONField(read_only=True, allow_null=True)
|
||||
pgh_diff = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_pgh_diff(self, obj) -> dict:
|
||||
"""Get diff from previous version if available."""
|
||||
if hasattr(obj, "diff_against_previous"):
|
||||
return obj.diff_against_previous()
|
||||
return {}
|
||||
|
||||
|
||||
class HistoryEntryOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for history entries."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
model_type = serializers.CharField()
|
||||
object_id = serializers.IntegerField()
|
||||
object_name = serializers.CharField()
|
||||
action = serializers.CharField()
|
||||
changes = serializers.JSONField()
|
||||
timestamp = serializers.DateTimeField()
|
||||
user = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField(allow_null=True))
|
||||
def get_user(self, obj) -> dict | None:
|
||||
if hasattr(obj, "user") and obj.user:
|
||||
return {
|
||||
"id": obj.user.id,
|
||||
"username": obj.user.username,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
class HistoryCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating history entries."""
|
||||
|
||||
action = serializers.CharField(max_length=50)
|
||||
description = serializers.CharField(max_length=500)
|
||||
metadata = serializers.JSONField(required=False)
|
||||
|
||||
|
||||
# === MODERATION SERIALIZERS ===
|
||||
|
||||
|
||||
class ModerationSubmissionSerializer(serializers.Serializer):
|
||||
"""Serializer for moderation submissions."""
|
||||
|
||||
submission_type = serializers.ChoiceField(
|
||||
choices=["EDIT", "PHOTO", "REVIEW"], help_text="Type of submission"
|
||||
)
|
||||
content_type = serializers.CharField(help_text="Content type being modified")
|
||||
object_id = serializers.IntegerField(help_text="ID of object being modified")
|
||||
changes = serializers.JSONField(help_text="Changes being submitted")
|
||||
reason = serializers.CharField(
|
||||
max_length=500,
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
help_text="Reason for the changes",
|
||||
)
|
||||
|
||||
|
||||
class ModerationSubmissionOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for moderation submission responses."""
|
||||
|
||||
status = serializers.CharField()
|
||||
message = serializers.CharField()
|
||||
submission_id = serializers.IntegerField(required=False)
|
||||
auto_approved = serializers.BooleanField(required=False)
|
||||
|
||||
|
||||
# === ROADTRIP SERIALIZERS ===
|
||||
|
||||
|
||||
class RoadtripParkSerializer(serializers.Serializer):
|
||||
"""Serializer for parks in roadtrip planning."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
latitude = serializers.FloatField()
|
||||
longitude = serializers.FloatField()
|
||||
coaster_count = serializers.IntegerField()
|
||||
status = serializers.CharField()
|
||||
|
||||
|
||||
class RoadtripCreateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for creating roadtrips."""
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
park_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
min_length=2,
|
||||
max_length=10,
|
||||
help_text="List of park IDs (2-10 parks)",
|
||||
)
|
||||
start_date = serializers.DateField(required=False)
|
||||
end_date = serializers.DateField(required=False)
|
||||
notes = serializers.CharField(max_length=1000, required=False, allow_blank=True)
|
||||
|
||||
def validate_park_ids(self, value):
|
||||
"""Validate park IDs."""
|
||||
if len(value) < 2:
|
||||
raise serializers.ValidationError("At least 2 parks are required")
|
||||
if len(value) > 10:
|
||||
raise serializers.ValidationError("Maximum 10 parks allowed")
|
||||
if len(set(value)) != len(value):
|
||||
raise serializers.ValidationError("Duplicate park IDs not allowed")
|
||||
return value
|
||||
|
||||
|
||||
class RoadtripOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for roadtrip responses."""
|
||||
|
||||
id = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
parks = RoadtripParkSerializer(many=True)
|
||||
total_distance_miles = serializers.FloatField()
|
||||
estimated_drive_time_hours = serializers.FloatField()
|
||||
route_coordinates = serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.FloatField())
|
||||
)
|
||||
created_at = serializers.DateTimeField()
|
||||
|
||||
|
||||
class GeocodeInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for geocoding requests."""
|
||||
|
||||
address = serializers.CharField(max_length=500, help_text="Address to geocode")
|
||||
|
||||
|
||||
class GeocodeOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for geocoding responses."""
|
||||
|
||||
status = serializers.CharField()
|
||||
coordinates = serializers.JSONField(required=False)
|
||||
formatted_address = serializers.CharField(required=False)
|
||||
|
||||
|
||||
# === DISTANCE CALCULATION SERIALIZERS ===
|
||||
class DistanceCalculationInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for distance calculation requests."""
|
||||
|
||||
park1_id = serializers.IntegerField(help_text="ID of first park")
|
||||
park2_id = serializers.IntegerField(help_text="ID of second park")
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate that park IDs are different."""
|
||||
if data["park1_id"] == data["park2_id"]:
|
||||
raise serializers.ValidationError("Park IDs must be different")
|
||||
return data
|
||||
|
||||
|
||||
class DistanceCalculationOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for distance calculation responses."""
|
||||
|
||||
status = serializers.CharField()
|
||||
distance_miles = serializers.FloatField(required=False)
|
||||
distance_km = serializers.FloatField(required=False)
|
||||
drive_time_hours = serializers.FloatField(required=False)
|
||||
message = serializers.CharField(required=False)
|
||||
159
backend/apps/api/v1/serializers/shared.py
Normal file
159
backend/apps/api/v1/serializers/shared.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Shared serializers and utilities for ThrillWiki API v1.
|
||||
|
||||
This module contains common serializers and helper classes used across multiple domains
|
||||
to avoid code duplication and maintain consistency.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
# Import models inside class methods to avoid Django initialization issues
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
# Define constants to avoid import-time model loading
|
||||
CATEGORY_CHOICES = [
|
||||
("RC", "Roller Coaster"),
|
||||
("FL", "Flat Ride"),
|
||||
("DR", "Dark Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
]
|
||||
|
||||
|
||||
# Placeholder for dynamic model choices - will be populated at runtime
|
||||
class ModelChoices:
|
||||
@staticmethod
|
||||
def get_ride_status_choices():
|
||||
try:
|
||||
from apps.rides.models import Ride
|
||||
|
||||
return Ride.STATUS_CHOICES
|
||||
except ImportError:
|
||||
return [("OPERATING", "Operating"), ("CLOSED", "Closed")]
|
||||
|
||||
@staticmethod
|
||||
def get_park_status_choices():
|
||||
try:
|
||||
from apps.parks.models import Park
|
||||
|
||||
return Park.STATUS_CHOICES
|
||||
except ImportError:
|
||||
return [("OPERATING", "Operating"), ("CLOSED", "Closed")]
|
||||
|
||||
@staticmethod
|
||||
def get_company_role_choices():
|
||||
try:
|
||||
from apps.parks.models import Company
|
||||
|
||||
return Company.CompanyRole.choices
|
||||
except ImportError:
|
||||
return [("OPERATOR", "Operator"), ("MANUFACTURER", "Manufacturer")]
|
||||
|
||||
@staticmethod
|
||||
def get_coaster_track_choices():
|
||||
try:
|
||||
from apps.rides.models import RollerCoasterStats
|
||||
|
||||
return RollerCoasterStats.TRACK_MATERIAL_CHOICES
|
||||
except ImportError:
|
||||
return [("STEEL", "Steel"), ("WOOD", "Wood")]
|
||||
|
||||
@staticmethod
|
||||
def get_coaster_type_choices():
|
||||
try:
|
||||
from apps.rides.models import RollerCoasterStats
|
||||
|
||||
return RollerCoasterStats.COASTER_TYPE_CHOICES
|
||||
except ImportError:
|
||||
return [("SITDOWN", "Sit Down"), ("INVERTED", "Inverted")]
|
||||
|
||||
@staticmethod
|
||||
def get_launch_choices():
|
||||
try:
|
||||
from apps.rides.models import RollerCoasterStats
|
||||
|
||||
return RollerCoasterStats.LAUNCH_CHOICES
|
||||
except ImportError:
|
||||
return [("CHAIN", "Chain Lift"), ("LAUNCH", "Launch")]
|
||||
|
||||
@staticmethod
|
||||
def get_top_list_categories():
|
||||
try:
|
||||
from apps.accounts.models import TopList
|
||||
|
||||
return TopList.Categories.choices
|
||||
except ImportError:
|
||||
return [("RC", "Roller Coasters"), ("PARKS", "Parks")]
|
||||
|
||||
@staticmethod
|
||||
def get_ride_post_closing_choices():
|
||||
try:
|
||||
from apps.rides.models import Ride
|
||||
|
||||
return Ride.POST_CLOSING_STATUS_CHOICES
|
||||
except ImportError:
|
||||
return [
|
||||
("DEMOLISHED", "Demolished"),
|
||||
("RELOCATED", "Relocated"),
|
||||
("SBNO", "Standing But Not Operating"),
|
||||
]
|
||||
|
||||
|
||||
class LocationOutputSerializer(serializers.Serializer):
|
||||
"""Shared serializer for location data."""
|
||||
|
||||
latitude = serializers.SerializerMethodField()
|
||||
longitude = serializers.SerializerMethodField()
|
||||
city = serializers.SerializerMethodField()
|
||||
state = serializers.SerializerMethodField()
|
||||
country = serializers.SerializerMethodField()
|
||||
formatted_address = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.FloatField(allow_null=True))
|
||||
def get_latitude(self, obj) -> float | None:
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.latitude
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.FloatField(allow_null=True))
|
||||
def get_longitude(self, obj) -> float | None:
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.longitude
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.CharField(allow_null=True))
|
||||
def get_city(self, obj) -> str | None:
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.city
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.CharField(allow_null=True))
|
||||
def get_state(self, obj) -> str | None:
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.state
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.CharField(allow_null=True))
|
||||
def get_country(self, obj) -> str | None:
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.country
|
||||
return None
|
||||
|
||||
@extend_schema_field(serializers.CharField())
|
||||
def get_formatted_address(self, obj) -> str:
|
||||
if hasattr(obj, "location") and obj.location:
|
||||
return obj.location.formatted_address
|
||||
return ""
|
||||
|
||||
|
||||
class CompanyOutputSerializer(serializers.Serializer):
|
||||
"""Shared serializer for company data."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
roles = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
Reference in New Issue
Block a user