feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -90,7 +90,6 @@ _ACCOUNTS_SYMBOLS: list[str] = [
"UserProfileOutputSerializer",
"UserProfileCreateInputSerializer",
"UserProfileUpdateInputSerializer",
"UserOutputSerializer",
"LoginInputSerializer",
"LoginOutputSerializer",

View File

@@ -187,6 +187,7 @@ class PublicUserSerializer(serializers.ModelSerializer):
Public user serializer for viewing other users' profiles.
Only exposes public information.
"""
profile = UserProfileSerializer(read_only=True)
class Meta:
@@ -228,37 +229,21 @@ class UserPreferencesSerializer(serializers.Serializer):
"""Serializer for user preferences and settings."""
theme_preference = RichChoiceFieldSerializer(
choice_group="theme_preferences",
domain="accounts",
help_text="User's theme preference"
)
email_notifications = serializers.BooleanField(
default=True, help_text="Whether to receive email notifications"
)
push_notifications = serializers.BooleanField(
default=False, help_text="Whether to receive push notifications"
choice_group="theme_preferences", domain="accounts", help_text="User's theme preference"
)
email_notifications = serializers.BooleanField(default=True, help_text="Whether to receive email notifications")
push_notifications = serializers.BooleanField(default=False, help_text="Whether to receive push notifications")
privacy_level = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
default="public",
help_text="Profile visibility level",
)
show_email = serializers.BooleanField(
default=False, help_text="Whether to show email on profile"
)
show_real_name = serializers.BooleanField(
default=True, help_text="Whether to show real name on profile"
)
show_statistics = serializers.BooleanField(
default=True, help_text="Whether to show ride statistics on profile"
)
allow_friend_requests = serializers.BooleanField(
default=True, help_text="Whether to allow friend requests"
)
allow_messages = serializers.BooleanField(
default=True, help_text="Whether to allow direct messages"
)
show_email = serializers.BooleanField(default=False, help_text="Whether to show email on profile")
show_real_name = serializers.BooleanField(default=True, help_text="Whether to show real name on profile")
show_statistics = serializers.BooleanField(default=True, help_text="Whether to show ride statistics on profile")
allow_friend_requests = serializers.BooleanField(default=True, help_text="Whether to allow friend requests")
allow_messages = serializers.BooleanField(default=True, help_text="Whether to allow direct messages")
# === NOTIFICATION SETTINGS SERIALIZERS ===
@@ -363,39 +348,17 @@ class PrivacySettingsSerializer(serializers.Serializer):
default="public",
help_text="Overall profile visibility",
)
show_email = serializers.BooleanField(
default=False, help_text="Show email address on profile"
)
show_real_name = serializers.BooleanField(
default=True, help_text="Show real name on profile"
)
show_join_date = serializers.BooleanField(
default=True, help_text="Show join date on profile"
)
show_statistics = serializers.BooleanField(
default=True, help_text="Show ride statistics on profile"
)
show_reviews = serializers.BooleanField(
default=True, help_text="Show reviews on profile"
)
show_photos = serializers.BooleanField(
default=True, help_text="Show uploaded photos on profile"
)
show_top_lists = serializers.BooleanField(
default=True, help_text="Show top lists on profile"
)
allow_friend_requests = serializers.BooleanField(
default=True, help_text="Allow others to send friend requests"
)
allow_messages = serializers.BooleanField(
default=True, help_text="Allow others to send direct messages"
)
allow_profile_comments = serializers.BooleanField(
default=False, help_text="Allow others to comment on profile"
)
search_visibility = serializers.BooleanField(
default=True, help_text="Allow profile to appear in search results"
)
show_email = serializers.BooleanField(default=False, help_text="Show email address on profile")
show_real_name = serializers.BooleanField(default=True, help_text="Show real name on profile")
show_join_date = serializers.BooleanField(default=True, help_text="Show join date on profile")
show_statistics = serializers.BooleanField(default=True, help_text="Show ride statistics on profile")
show_reviews = serializers.BooleanField(default=True, help_text="Show reviews on profile")
show_photos = serializers.BooleanField(default=True, help_text="Show uploaded photos on profile")
show_top_lists = serializers.BooleanField(default=True, help_text="Show top lists on profile")
allow_friend_requests = serializers.BooleanField(default=True, help_text="Allow others to send friend requests")
allow_messages = serializers.BooleanField(default=True, help_text="Allow others to send direct messages")
allow_profile_comments = serializers.BooleanField(default=False, help_text="Allow others to comment on profile")
search_visibility = serializers.BooleanField(default=True, help_text="Allow profile to appear in search results")
activity_visibility = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
@@ -431,21 +394,13 @@ class SecuritySettingsSerializer(serializers.Serializer):
two_factor_enabled = serializers.BooleanField(
default=False, help_text="Whether two-factor authentication is enabled"
)
login_notifications = serializers.BooleanField(
default=True, help_text="Send notifications for new logins"
)
login_notifications = serializers.BooleanField(default=True, help_text="Send notifications for new logins")
session_timeout = serializers.IntegerField(
default=30, min_value=5, max_value=180, help_text="Session timeout in days"
)
require_password_change = serializers.BooleanField(
default=False, help_text="Whether password change is required"
)
last_password_change = serializers.DateTimeField(
read_only=True, help_text="When password was last changed"
)
active_sessions = serializers.IntegerField(
read_only=True, help_text="Number of active sessions"
)
require_password_change = serializers.BooleanField(default=False, help_text="Whether password change is required")
last_password_change = serializers.DateTimeField(read_only=True, help_text="When password was last changed")
active_sessions = serializers.IntegerField(read_only=True, help_text="Number of active sessions")
login_history_retention = serializers.IntegerField(
default=90,
min_value=30,
@@ -699,7 +654,7 @@ class ThemePreferenceSerializer(serializers.ModelSerializer):
"id": 1,
"notification_type": "submission_approved",
"title": "Your submission has been approved!",
"message": "Your photo submission for Cedar Point has been approved and is now live on the site.",
"detail": "Your photo submission for Cedar Point has been approved and is now live on the site.",
"priority": "normal",
"is_read": False,
"read_at": None,
@@ -866,15 +821,11 @@ class MarkNotificationsReadSerializer(serializers.Serializer):
def validate_notification_ids(self, value):
"""Validate that all notification IDs belong to the requesting user."""
user = self.context["request"].user
valid_ids = UserNotification.objects.filter(
id__in=value, user=user
).values_list("id", flat=True)
valid_ids = UserNotification.objects.filter(id__in=value, user=user).values_list("id", flat=True)
invalid_ids = set(value) - set(valid_ids)
if invalid_ids:
raise serializers.ValidationError(
f"Invalid notification IDs: {list(invalid_ids)}"
)
raise serializers.ValidationError(f"Invalid notification IDs: {list(invalid_ids)}")
return value
@@ -901,9 +852,8 @@ class AvatarUploadSerializer(serializers.Serializer):
raise serializers.ValidationError("No file provided")
# Check file size constraints (max 10MB for Cloudflare Images)
if hasattr(value, 'size') and value.size > 10 * 1024 * 1024:
raise serializers.ValidationError(
"Image file too large. Maximum size is 10MB.")
if hasattr(value, "size") and value.size > 10 * 1024 * 1024:
raise serializers.ValidationError("Image file too large. Maximum size is 10MB.")
# Try to validate with PIL
try:
@@ -926,13 +876,13 @@ class AvatarUploadSerializer(serializers.Serializer):
# Check image dimensions (max 12,000x12,000 for Cloudflare Images)
if image.size[0] > 12000 or image.size[1] > 12000:
raise serializers.ValidationError(
"Image dimensions too large. Maximum is 12,000x12,000 pixels.")
raise serializers.ValidationError("Image dimensions too large. Maximum is 12,000x12,000 pixels.")
# Check if it's a supported format
if image.format not in ['JPEG', 'PNG', 'GIF', 'WEBP']:
if image.format not in ["JPEG", "PNG", "GIF", "WEBP"]:
raise serializers.ValidationError(
f"Unsupported image format: {image.format}. Supported formats: JPEG, PNG, GIF, WebP.")
f"Unsupported image format: {image.format}. Supported formats: JPEG, PNG, GIF, WebP."
)
except serializers.ValidationError:
raise # Re-raise validation errors

View File

@@ -97,7 +97,7 @@ class LoginInputSerializer(serializers.Serializer):
password=password,
)
if not user:
if not user: # noqa: SIM102
# Try email-based authentication if username failed
if "@" in username:
try:
@@ -138,7 +138,7 @@ class LoginInputSerializer(serializers.Serializer):
"first_name": "John",
"last_name": "Doe",
},
"message": "Login successful",
"detail": "Login successful",
},
)
]
@@ -213,7 +213,7 @@ class SignupInputSerializer(serializers.ModelSerializer):
try:
validate_password(value)
except DjangoValidationError as e:
raise serializers.ValidationError(list(e.messages))
raise serializers.ValidationError(list(e.messages)) from None
return value
def validate(self, attrs):
@@ -253,7 +253,7 @@ class SignupInputSerializer(serializers.ModelSerializer):
"first_name": "Jane",
"last_name": "Smith",
},
"message": "Registration successful",
"detail": "Registration successful",
},
)
]
@@ -276,7 +276,7 @@ class SignupOutputSerializer(serializers.Serializer):
summary="Example logout response",
description="Successful logout response",
value={
"message": "Logout successful",
"detail": "Logout successful",
},
)
]
@@ -318,9 +318,9 @@ class PasswordResetInputSerializer(serializers.Serializer):
"""Send password reset email."""
email = self.validated_data["email"] # type: ignore[index]
try:
_user = UserModel.objects.get(email=email)
# Check if email exists (but don't reveal the result for security)
UserModel.objects.get(email=email)
# Here you would typically send a password reset email
# For now, we'll just pass
pass
except UserModel.DoesNotExist:
# Don't reveal if email exists for security
@@ -393,7 +393,7 @@ class PasswordChangeInputSerializer(serializers.Serializer):
try:
validate_password(value, user=self.context["request"].user)
except DjangoValidationError as e:
raise serializers.ValidationError(list(e.messages))
raise serializers.ValidationError(list(e.messages)) from None
return value
def validate(self, attrs):
@@ -492,6 +492,4 @@ class AuthStatusOutputSerializer(serializers.Serializer):
"""Output serializer for authentication status."""
authenticated = serializers.BooleanField(help_text="Whether user is authenticated")
user = UserOutputSerializer(
allow_null=True, help_text="User information if authenticated"
)
user = UserOutputSerializer(allow_null=True, help_text="User information if authenticated")

View File

@@ -112,10 +112,7 @@ class RideModelDetailOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
description = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
category = RichChoiceFieldSerializer(choice_group="categories", domain="rides")
# Manufacturer info
manufacturer = serializers.SerializerMethodField()

View File

@@ -99,9 +99,7 @@ class ParkHistoryOutputSerializer(serializers.Serializer):
"slug": park.slug,
"status": park.status,
"opening_date": (
park.opening_date.isoformat()
if hasattr(park, "opening_date") and park.opening_date
else None
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),
@@ -143,9 +141,7 @@ class RideHistoryOutputSerializer(serializers.Serializer):
"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.opening_date.isoformat() if hasattr(ride, "opening_date") and ride.opening_date else None
),
"ride_type": getattr(ride, "ride_type", "Unknown"),
}

View File

@@ -79,16 +79,12 @@ class MapLocationSerializer(serializers.Serializer):
return {
"coaster_count": obj.coaster_count or 0,
"ride_count": obj.ride_count or 0,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"average_rating": (float(obj.average_rating) if obj.average_rating else None),
}
elif obj._meta.model_name == "ride":
return {
"category": obj.get_category_display() if obj.category else None,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"average_rating": (float(obj.average_rating) if obj.average_rating else None),
"park_name": obj.park.name if obj.park else None,
}
return {}
@@ -339,24 +335,16 @@ class MapLocationDetailSerializer(serializers.Serializer):
return {
"coaster_count": obj.coaster_count or 0,
"ride_count": obj.ride_count or 0,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"average_rating": (float(obj.average_rating) if obj.average_rating else None),
"size_acres": float(obj.size_acres) if obj.size_acres else None,
"opening_date": (
obj.opening_date.isoformat() if obj.opening_date else None
),
"opening_date": (obj.opening_date.isoformat() if obj.opening_date else None),
}
elif obj._meta.model_name == "ride":
return {
"category": obj.get_category_display() if obj.category else None,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"average_rating": (float(obj.average_rating) if obj.average_rating else None),
"park_name": obj.park.name if obj.park else None,
"opening_date": (
obj.opening_date.isoformat() if obj.opening_date else None
),
"opening_date": (obj.opening_date.isoformat() if obj.opening_date else None),
"manufacturer": obj.manufacturer.name if obj.manufacturer else None,
}
return {}
@@ -382,9 +370,7 @@ class MapBoundsInputSerializer(serializers.Serializer):
def validate(self, attrs):
"""Validate that bounds make geographic sense."""
if attrs["north"] <= attrs["south"]:
raise serializers.ValidationError(
"North bound must be greater than south bound"
)
raise serializers.ValidationError("North bound must be greater than south bound")
# Handle longitude wraparound (e.g., crossing the international date line)
# For now, we'll require west < east for simplicity

View File

@@ -31,9 +31,7 @@ class PhotoUploadInputSerializer(serializers.Serializer):
allow_blank=True,
help_text="Alt text for accessibility",
)
is_primary = serializers.BooleanField(
default=False, help_text="Whether this should be the primary photo"
)
is_primary = serializers.BooleanField(default=False, help_text="Whether this should be the primary photo")
@extend_schema_serializer(
@@ -89,9 +87,7 @@ class PhotoDetailOutputSerializer(serializers.Serializer):
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
)(),
"display_name": getattr(obj.uploaded_by, "get_display_name", lambda: obj.uploaded_by.username)(),
}

View File

@@ -24,12 +24,8 @@ class ParkStatsOutputSerializer(serializers.Serializer):
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
)
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())
@@ -50,12 +46,8 @@ class RideStatsOutputSerializer(serializers.Serializer):
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
)
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())
@@ -91,10 +83,7 @@ class ParkReviewOutputSerializer(serializers.Serializer):
class HealthCheckOutputSerializer(serializers.Serializer):
"""Output serializer for health check responses."""
status = RichChoiceFieldSerializer(
choice_group="health_statuses",
domain="core"
)
status = RichChoiceFieldSerializer(choice_group="health_statuses", domain="core")
timestamp = serializers.DateTimeField()
version = serializers.CharField()
environment = serializers.CharField()
@@ -115,9 +104,6 @@ class PerformanceMetricsOutputSerializer(serializers.Serializer):
class SimpleHealthOutputSerializer(serializers.Serializer):
"""Output serializer for simple health check."""
status = RichChoiceFieldSerializer(
choice_group="simple_health_statuses",
domain="core"
)
status = RichChoiceFieldSerializer(choice_group="simple_health_statuses", domain="core")
timestamp = serializers.DateTimeField()
error = serializers.CharField(required=False)

View File

@@ -29,14 +29,10 @@ from apps.parks.models.reviews import ParkReview
"user": {
"username": "park_fan",
"display_name": "Park Fan",
"avatar_url": "https://example.com/avatar.jpg"
"avatar_url": "https://example.com/avatar.jpg",
},
"park": {
"id": 101,
"name": "Cedar Point",
"slug": "cedar-point"
}
}
"park": {"id": 101, "name": "Cedar Point", "slug": "cedar-point"},
},
)
]
)
@@ -145,8 +141,7 @@ class ParkReviewStatsOutputSerializer(serializers.Serializer):
pending_reviews = serializers.IntegerField()
average_rating = serializers.FloatField(allow_null=True)
rating_distribution = serializers.DictField(
child=serializers.IntegerField(),
help_text="Count of reviews by rating (1-10)"
child=serializers.IntegerField(), help_text="Count of reviews by rating (1-10)"
)
recent_reviews = serializers.IntegerField()
@@ -154,20 +149,15 @@ class ParkReviewStatsOutputSerializer(serializers.Serializer):
class ParkReviewModerationInputSerializer(serializers.Serializer):
"""Input serializer for review moderation operations."""
review_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of review IDs to moderate"
)
review_ids = serializers.ListField(child=serializers.IntegerField(), help_text="List of review IDs to moderate")
action = serializers.ChoiceField(
choices=[
("publish", "Publish"),
("unpublish", "Unpublish"),
("delete", "Delete"),
],
help_text="Moderation action to perform"
help_text="Moderation action to perform",
)
moderation_notes = serializers.CharField(
required=False,
allow_blank=True,
help_text="Optional notes about the moderation action"
required=False, allow_blank=True, help_text="Optional notes about the moderation action"
)

View File

@@ -52,16 +52,11 @@ class ParkListOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
status = RichChoiceFieldSerializer(choice_group="statuses", domain="parks")
description = serializers.CharField()
# Statistics
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, allow_null=True
)
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)
@@ -145,25 +140,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
status = RichChoiceFieldSerializer(choice_group="statuses", domain="parks")
description = serializers.CharField()
# Details
opening_date = serializers.DateField(allow_null=True)
closing_date = serializers.DateField(allow_null=True)
operating_season = serializers.CharField()
size_acres = serializers.DecimalField(
max_digits=10, decimal_places=2, allow_null=True
)
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
)
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)
@@ -211,9 +199,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"""Get all approved photos for this park."""
from apps.parks.models import ParkPhoto
photos = ParkPhoto.objects.filter(park=obj, is_approved=True).order_by(
"-is_primary", "-created_at"
)[
photos = ParkPhoto.objects.filter(park=obj, is_approved=True).order_by("-is_primary", "-created_at")[
:10
] # Limit to 10 photos
@@ -228,7 +214,9 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, photo.caption, photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "public"),
@@ -246,9 +234,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
from apps.parks.models import ParkPhoto
try:
photo = ParkPhoto.objects.filter(
park=obj, is_primary=True, is_approved=True
).first()
photo = ParkPhoto.objects.filter(park=obj, is_primary=True, is_approved=True).first()
if photo and photo.image:
return {
@@ -261,7 +247,9 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "thumbnail"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, photo.caption, photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, photo.caption, photo.pk, "public"),
@@ -289,10 +277,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.banner_image.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.banner_image.caption, obj.banner_image.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, obj.banner_image.caption, obj.banner_image.pk, "public"
),
},
"caption": obj.banner_image.caption,
"alt_text": obj.banner_image.alt_text,
@@ -303,9 +299,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
try:
latest_photo = (
ParkPhoto.objects.filter(
park=obj, is_approved=True, image__isnull=False
)
ParkPhoto.objects.filter(park=obj, is_approved=True, image__isnull=False)
.order_by("-created_at")
.first()
)
@@ -321,10 +315,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "public"
),
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
@@ -350,10 +352,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(obj.card_image.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, obj.card_image.caption, obj.card_image.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, obj.card_image.caption, obj.card_image.pk, "public"
),
},
"caption": obj.card_image.caption,
"alt_text": obj.card_image.alt_text,
@@ -364,9 +374,7 @@ class ParkDetailOutputSerializer(serializers.Serializer):
try:
latest_photo = (
ParkPhoto.objects.filter(
park=obj, is_approved=True, image__isnull=False
)
ParkPhoto.objects.filter(park=obj, is_approved=True, image__isnull=False)
.order_by("-created_at")
.first()
)
@@ -382,10 +390,18 @@ class ParkDetailOutputSerializer(serializers.Serializer):
"public": MediaURLService.get_cloudflare_url_with_fallback(latest_photo.image, "public"),
},
"friendly_urls": {
"thumbnail": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"),
"medium": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "medium"),
"large": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "large"),
"public": MediaURLService.generate_park_photo_url(obj.slug, latest_photo.caption, latest_photo.pk, "public"),
"thumbnail": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "thumbnail"
),
"medium": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "medium"
),
"large": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "large"
),
"public": MediaURLService.generate_park_photo_url(
obj.slug, latest_photo.caption, latest_photo.pk, "public"
),
},
"caption": latest_photo.caption,
"alt_text": latest_photo.alt_text,
@@ -417,7 +433,7 @@ class ParkImageSettingsInputSerializer(serializers.Serializer):
# The park will be validated in the view
return value
except ParkPhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
raise serializers.ValidationError("Photo not found") from None
return value
def validate_card_image_id(self, value):
@@ -430,7 +446,7 @@ class ParkImageSettingsInputSerializer(serializers.Serializer):
# The park will be validated in the view
return value
except ParkPhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
raise serializers.ValidationError("Photo not found") from None
return value
@@ -439,19 +455,13 @@ class ParkCreateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
status = serializers.ChoiceField(
choices=ModelChoices.get_park_status_choices(), default="OPERATING"
)
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
)
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
@@ -466,9 +476,7 @@ class ParkCreateInputSerializer(serializers.Serializer):
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"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs
@@ -478,19 +486,13 @@ class ParkUpdateInputSerializer(serializers.Serializer):
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
)
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
)
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
@@ -503,9 +505,7 @@ class ParkUpdateInputSerializer(serializers.Serializer):
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"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs
@@ -537,12 +537,8 @@ class ParkFilterInputSerializer(serializers.Serializer):
)
# 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
)
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)
@@ -625,9 +621,7 @@ class ParkAreaCreateInputSerializer(serializers.Serializer):
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"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs
@@ -646,9 +640,7 @@ class ParkAreaUpdateInputSerializer(serializers.Serializer):
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"
)
raise serializers.ValidationError("Closing date cannot be before opening date")
return attrs

View File

@@ -12,9 +12,7 @@ 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
)
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)
@@ -78,9 +76,7 @@ class ParkPhotoUpdateInputSerializer(serializers.ModelSerializer):
class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for park photo lists."""
uploaded_by_username = serializers.CharField(
source="uploaded_by.username", read_only=True
)
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
class Meta:
model = ParkPhoto
@@ -99,12 +95,8 @@ class ParkPhotoListOutputSerializer(serializers.ModelSerializer):
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"
)
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):

View File

@@ -8,35 +8,33 @@ from apps.rides.models.credits import RideCredit
class RideCreditSerializer(serializers.ModelSerializer):
"""Serializer for user ride credits."""
ride_id = serializers.PrimaryKeyRelatedField(
queryset=Ride.objects.all(), source='ride', write_only=True
)
ride_id = serializers.PrimaryKeyRelatedField(queryset=Ride.objects.all(), source="ride", write_only=True)
ride = RideListOutputSerializer(read_only=True)
class Meta:
model = RideCredit
fields = [
'id',
'ride',
'ride_id',
'count',
'rating',
'first_ridden_at',
'last_ridden_at',
'notes',
'display_order',
'created_at',
'updated_at',
"id",
"ride",
"ride_id",
"count",
"rating",
"first_ridden_at",
"last_ridden_at",
"notes",
"display_order",
"created_at",
"updated_at",
]
read_only_fields = ['id', 'created_at', 'updated_at']
read_only_fields = ["id", "created_at", "updated_at"]
def validate(self, attrs):
"""
Validate data.
"""
# Ensure dates make sense
first = attrs.get('first_ridden_at')
last = attrs.get('last_ridden_at')
first = attrs.get("first_ridden_at")
last = attrs.get("last_ridden_at")
if first and last and last < first:
raise serializers.ValidationError("Last ridden date cannot be before first ridden date.")
@@ -44,6 +42,6 @@ class RideCreditSerializer(serializers.ModelSerializer):
def create(self, validated_data):
"""Create a new ride credit."""
user = self.context['request'].user
validated_data['user'] = user
user = self.context["request"].user
validated_data["user"] = user
return super().create(validated_data)

View File

@@ -80,18 +80,10 @@ class RideModelVariantOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
description = serializers.CharField()
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
min_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, allow_null=True)
max_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, allow_null=True)
min_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, allow_null=True)
max_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, allow_null=True)
distinguishing_features = serializers.CharField()
@@ -134,20 +126,14 @@ class RideModelListOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
category = RichChoiceFieldSerializer(choice_group="categories", domain="rides")
description = serializers.CharField()
# Manufacturer info
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
# Market info
target_market = RichChoiceFieldSerializer(
choice_group="target_markets",
domain="rides"
)
target_market = RichChoiceFieldSerializer(choice_group="target_markets", domain="rides")
is_discontinued = serializers.BooleanField()
total_installations = serializers.IntegerField()
first_installation_year = serializers.IntegerField(allow_null=True)
@@ -258,18 +244,10 @@ class RideModelDetailOutputSerializer(serializers.Serializer):
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
# Technical specifications
typical_height_range_min_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
typical_height_range_max_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
typical_speed_range_min_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
typical_speed_range_max_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, allow_null=True
)
typical_height_range_min_ft = serializers.DecimalField(max_digits=6, decimal_places=2, allow_null=True)
typical_height_range_max_ft = serializers.DecimalField(max_digits=6, decimal_places=2, allow_null=True)
typical_speed_range_min_mph = serializers.DecimalField(max_digits=5, decimal_places=2, allow_null=True)
typical_speed_range_max_mph = serializers.DecimalField(max_digits=5, decimal_places=2, allow_null=True)
typical_capacity_range_min = serializers.IntegerField(allow_null=True)
typical_capacity_range_max = serializers.IntegerField(allow_null=True)
@@ -343,9 +321,7 @@ class RideModelCreateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
category = serializers.ChoiceField(
choices=ModelChoices.get_ride_category_choices(), allow_blank=True, default=""
)
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices(), allow_blank=True, default="")
# Required manufacturer
manufacturer_id = serializers.IntegerField()
@@ -363,32 +339,18 @@ class RideModelCreateInputSerializer(serializers.Serializer):
typical_speed_range_max_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
typical_capacity_range_min = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
typical_capacity_range_max = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
typical_capacity_range_min = serializers.IntegerField(required=False, allow_null=True, min_value=1)
typical_capacity_range_max = serializers.IntegerField(required=False, allow_null=True, min_value=1)
# Design characteristics
track_type = serializers.CharField(max_length=100, allow_blank=True, default="")
support_structure = serializers.CharField(
max_length=100, allow_blank=True, default=""
)
train_configuration = serializers.CharField(
max_length=200, allow_blank=True, default=""
)
restraint_system = serializers.CharField(
max_length=100, allow_blank=True, default=""
)
support_structure = serializers.CharField(max_length=100, allow_blank=True, default="")
train_configuration = serializers.CharField(max_length=200, allow_blank=True, default="")
restraint_system = serializers.CharField(max_length=100, allow_blank=True, default="")
# Market information
first_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
last_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
first_installation_year = serializers.IntegerField(required=False, allow_null=True, min_value=1800, max_value=2100)
last_installation_year = serializers.IntegerField(required=False, allow_null=True, min_value=1800, max_value=2100)
is_discontinued = serializers.BooleanField(default=False)
# Design features
@@ -406,36 +368,28 @@ class RideModelCreateInputSerializer(serializers.Serializer):
max_height = attrs.get("typical_height_range_max_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
raise serializers.ValidationError("Minimum height cannot be greater than maximum height")
# Speed range validation
min_speed = attrs.get("typical_speed_range_min_mph")
max_speed = attrs.get("typical_speed_range_max_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
raise serializers.ValidationError("Minimum speed cannot be greater than maximum speed")
# Capacity range validation
min_capacity = attrs.get("typical_capacity_range_min")
max_capacity = attrs.get("typical_capacity_range_max")
if min_capacity and max_capacity and min_capacity > max_capacity:
raise serializers.ValidationError(
"Minimum capacity cannot be greater than maximum capacity"
)
raise serializers.ValidationError("Minimum capacity cannot be greater than maximum capacity")
# Installation years validation
first_year = attrs.get("first_installation_year")
last_year = attrs.get("last_installation_year")
if first_year and last_year and first_year > last_year:
raise serializers.ValidationError(
"First installation year cannot be after last installation year"
)
raise serializers.ValidationError("First installation year cannot be after last installation year")
return attrs
@@ -467,32 +421,18 @@ class RideModelUpdateInputSerializer(serializers.Serializer):
typical_speed_range_max_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
typical_capacity_range_min = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
typical_capacity_range_max = serializers.IntegerField(
required=False, allow_null=True, min_value=1
)
typical_capacity_range_min = serializers.IntegerField(required=False, allow_null=True, min_value=1)
typical_capacity_range_max = serializers.IntegerField(required=False, allow_null=True, min_value=1)
# Design characteristics
track_type = serializers.CharField(max_length=100, allow_blank=True, required=False)
support_structure = serializers.CharField(
max_length=100, allow_blank=True, required=False
)
train_configuration = serializers.CharField(
max_length=200, allow_blank=True, required=False
)
restraint_system = serializers.CharField(
max_length=100, allow_blank=True, required=False
)
support_structure = serializers.CharField(max_length=100, allow_blank=True, required=False)
train_configuration = serializers.CharField(max_length=200, allow_blank=True, required=False)
restraint_system = serializers.CharField(max_length=100, allow_blank=True, required=False)
# Market information
first_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
last_installation_year = serializers.IntegerField(
required=False, allow_null=True, min_value=1800, max_value=2100
)
first_installation_year = serializers.IntegerField(required=False, allow_null=True, min_value=1800, max_value=2100)
last_installation_year = serializers.IntegerField(required=False, allow_null=True, min_value=1800, max_value=2100)
is_discontinued = serializers.BooleanField(required=False)
# Design features
@@ -510,36 +450,28 @@ class RideModelUpdateInputSerializer(serializers.Serializer):
max_height = attrs.get("typical_height_range_max_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
raise serializers.ValidationError("Minimum height cannot be greater than maximum height")
# Speed range validation
min_speed = attrs.get("typical_speed_range_min_mph")
max_speed = attrs.get("typical_speed_range_max_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
raise serializers.ValidationError("Minimum speed cannot be greater than maximum speed")
# Capacity range validation
min_capacity = attrs.get("typical_capacity_range_min")
max_capacity = attrs.get("typical_capacity_range_max")
if min_capacity and max_capacity and min_capacity > max_capacity:
raise serializers.ValidationError(
"Minimum capacity cannot be greater than maximum capacity"
)
raise serializers.ValidationError("Minimum capacity cannot be greater than maximum capacity")
# Installation years validation
first_year = attrs.get("first_installation_year")
last_year = attrs.get("last_installation_year")
if first_year and last_year and first_year > last_year:
raise serializers.ValidationError(
"First installation year cannot be after last installation year"
)
raise serializers.ValidationError("First installation year cannot be after last installation year")
return attrs
@@ -551,9 +483,7 @@ class RideModelFilterInputSerializer(serializers.Serializer):
search = serializers.CharField(required=False, allow_blank=True)
# Category filter
category = serializers.MultipleChoiceField(
choices=ModelChoices.get_ride_category_choices(), required=False
)
category = serializers.MultipleChoiceField(choices=ModelChoices.get_ride_category_choices(), required=False)
# Manufacturer filter
manufacturer_id = serializers.IntegerField(required=False)
@@ -576,20 +506,12 @@ class RideModelFilterInputSerializer(serializers.Serializer):
min_installations = serializers.IntegerField(required=False, min_value=0)
# Height filters
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False
)
min_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False)
max_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False)
# Speed filters
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False
)
min_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False)
max_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False)
# Ordering
ordering = serializers.ChoiceField(
@@ -621,18 +543,10 @@ class RideModelVariantCreateInputSerializer(serializers.Serializer):
description = serializers.CharField(allow_blank=True, default="")
# Variant-specific specifications
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
min_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
max_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
min_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False, allow_null=True)
max_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False, allow_null=True)
# Distinguishing features
distinguishing_features = serializers.CharField(allow_blank=True, default="")
@@ -644,18 +558,14 @@ class RideModelVariantCreateInputSerializer(serializers.Serializer):
max_height = attrs.get("max_height_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
raise serializers.ValidationError("Minimum height cannot be greater than maximum height")
# Speed range validation
min_speed = attrs.get("min_speed_mph")
max_speed = attrs.get("max_speed_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
raise serializers.ValidationError("Minimum speed cannot be greater than maximum speed")
return attrs
@@ -667,18 +577,10 @@ class RideModelVariantUpdateInputSerializer(serializers.Serializer):
description = serializers.CharField(allow_blank=True, required=False)
# Variant-specific specifications
min_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
max_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
min_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
max_speed_mph = serializers.DecimalField(
max_digits=5, decimal_places=2, required=False, allow_null=True
)
min_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
max_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
min_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False, allow_null=True)
max_speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False, allow_null=True)
# Distinguishing features
distinguishing_features = serializers.CharField(allow_blank=True, required=False)
@@ -690,18 +592,14 @@ class RideModelVariantUpdateInputSerializer(serializers.Serializer):
max_height = attrs.get("max_height_ft")
if min_height and max_height and min_height > max_height:
raise serializers.ValidationError(
"Minimum height cannot be greater than maximum height"
)
raise serializers.ValidationError("Minimum height cannot be greater than maximum height")
# Speed range validation
min_speed = attrs.get("min_speed_mph")
max_speed = attrs.get("max_speed_mph")
if min_speed and max_speed and min_speed > max_speed:
raise serializers.ValidationError(
"Minimum speed cannot be greater than maximum speed"
)
raise serializers.ValidationError("Minimum speed cannot be greater than maximum speed")
return attrs
@@ -713,9 +611,7 @@ class RideModelTechnicalSpecCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating ride model technical specifications."""
ride_model_id = serializers.IntegerField()
spec_category = serializers.ChoiceField(
choices=ModelChoices.get_technical_spec_category_choices()
)
spec_category = serializers.ChoiceField(choices=ModelChoices.get_technical_spec_category_choices())
spec_name = serializers.CharField(max_length=100)
spec_value = serializers.CharField(max_length=255)
spec_unit = serializers.CharField(max_length=20, allow_blank=True, default="")
@@ -765,13 +661,9 @@ class RideModelPhotoUpdateInputSerializer(serializers.Serializer):
required=False,
)
is_primary = serializers.BooleanField(required=False)
photographer = serializers.CharField(
max_length=255, allow_blank=True, required=False
)
photographer = serializers.CharField(max_length=255, allow_blank=True, required=False)
source = serializers.CharField(max_length=255, allow_blank=True, required=False)
copyright_info = serializers.CharField(
max_length=255, allow_blank=True, required=False
)
copyright_info = serializers.CharField(max_length=255, allow_blank=True, required=False)
# === RIDE MODEL STATS SERIALIZERS ===
@@ -784,15 +676,9 @@ class RideModelStatsOutputSerializer(serializers.Serializer):
total_installations = serializers.IntegerField()
active_manufacturers = serializers.IntegerField()
discontinued_models = serializers.IntegerField()
by_category = serializers.DictField(
child=serializers.IntegerField(), help_text="Model counts by category"
)
by_category = serializers.DictField(child=serializers.IntegerField(), help_text="Model counts by category")
by_target_market = serializers.DictField(
child=serializers.IntegerField(), help_text="Model counts by target market"
)
by_manufacturer = serializers.DictField(
child=serializers.IntegerField(), help_text="Model counts by manufacturer"
)
recent_models = serializers.IntegerField(
help_text="Models created in the last 30 days"
)
by_manufacturer = serializers.DictField(child=serializers.IntegerField(), help_text="Model counts by manufacturer")
recent_models = serializers.IntegerField(help_text="Models created in the last 30 days")

View File

@@ -54,19 +54,11 @@ class ReviewUserSerializer(serializers.ModelSerializer):
"id": 456,
"username": "coaster_fan",
"display_name": "Coaster Fan",
"avatar_url": "https://example.com/avatar.jpg"
"avatar_url": "https://example.com/avatar.jpg",
},
"ride": {
"id": 789,
"name": "Steel Vengeance",
"slug": "steel-vengeance"
},
"park": {
"id": 101,
"name": "Cedar Point",
"slug": "cedar-point"
}
}
"ride": {"id": 789, "name": "Steel Vengeance", "slug": "steel-vengeance"},
"park": {"id": 101, "name": "Cedar Point", "slug": "cedar-point"},
},
)
]
)
@@ -191,8 +183,7 @@ class RideReviewStatsOutputSerializer(serializers.Serializer):
pending_reviews = serializers.IntegerField()
average_rating = serializers.FloatField(allow_null=True)
rating_distribution = serializers.DictField(
child=serializers.IntegerField(),
help_text="Count of reviews by rating (1-10)"
child=serializers.IntegerField(), help_text="Count of reviews by rating (1-10)"
)
recent_reviews = serializers.IntegerField()
@@ -200,20 +191,15 @@ class RideReviewStatsOutputSerializer(serializers.Serializer):
class RideReviewModerationInputSerializer(serializers.Serializer):
"""Input serializer for review moderation operations."""
review_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of review IDs to moderate"
)
review_ids = serializers.ListField(child=serializers.IntegerField(), help_text="List of review IDs to moderate")
action = serializers.ChoiceField(
choices=[
("publish", "Publish"),
("unpublish", "Unpublish"),
("delete", "Delete"),
],
help_text="Moderation action to perform"
help_text="Moderation action to perform",
)
moderation_notes = serializers.CharField(
required=False,
allow_blank=True,
help_text="Optional notes about the moderation action"
required=False, allow_blank=True, help_text="Optional notes about the moderation action"
)

View File

@@ -81,23 +81,15 @@ class RideListOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="rides"
)
category = RichChoiceFieldSerializer(choice_group="categories", domain="rides")
status = RichChoiceFieldSerializer(choice_group="statuses", domain="rides")
description = serializers.CharField()
# Park info
park = RideParkOutputSerializer()
# Statistics
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, allow_null=True
)
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
capacity_per_hour = serializers.IntegerField(allow_null=True)
# Dates
@@ -178,18 +170,10 @@ class RideDetailOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="rides"
)
category = RichChoiceFieldSerializer(choice_group="categories", domain="rides")
status = RichChoiceFieldSerializer(choice_group="statuses", domain="rides")
post_closing_status = RichChoiceFieldSerializer(
choice_group="post_closing_statuses",
domain="rides",
allow_null=True
choice_group="post_closing_statuses", domain="rides", allow_null=True
)
description = serializers.CharField()
@@ -209,9 +193,7 @@ class RideDetailOutputSerializer(serializers.Serializer):
ride_duration_seconds = serializers.IntegerField(allow_null=True)
# Statistics
average_rating = serializers.DecimalField(
max_digits=3, decimal_places=2, allow_null=True
)
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
# Companies
manufacturer = serializers.SerializerMethodField()
@@ -273,9 +255,7 @@ class RideDetailOutputSerializer(serializers.Serializer):
"""Get all approved photos for this ride."""
from apps.rides.models import RidePhoto
photos = RidePhoto.objects.filter(ride=obj, is_approved=True).order_by(
"-is_primary", "-created_at"
)[
photos = RidePhoto.objects.filter(ride=obj, is_approved=True).order_by("-is_primary", "-created_at")[
:10
] # Limit to 10 photos
@@ -285,9 +265,7 @@ class RideDetailOutputSerializer(serializers.Serializer):
"image_url": photo.image.url if photo.image else None,
"image_variants": (
{
"thumbnail": (
f"{photo.image.url}/thumbnail" if photo.image else None
),
"thumbnail": (f"{photo.image.url}/thumbnail" if photo.image else None),
"medium": f"{photo.image.url}/medium" if photo.image else None,
"large": f"{photo.image.url}/large" if photo.image else None,
"public": f"{photo.image.url}/public" if photo.image else None,
@@ -309,9 +287,7 @@ class RideDetailOutputSerializer(serializers.Serializer):
from apps.rides.models import RidePhoto
try:
photo = RidePhoto.objects.filter(
ride=obj, is_primary=True, is_approved=True
).first()
photo = RidePhoto.objects.filter(ride=obj, is_primary=True, is_approved=True).first()
if photo and photo.image:
return {
@@ -356,9 +332,7 @@ class RideDetailOutputSerializer(serializers.Serializer):
try:
latest_photo = (
RidePhoto.objects.filter(
ride=obj, is_approved=True, image__isnull=False
)
RidePhoto.objects.filter(ride=obj, is_approved=True, image__isnull=False)
.order_by("-created_at")
.first()
)
@@ -407,9 +381,7 @@ class RideDetailOutputSerializer(serializers.Serializer):
try:
latest_photo = (
RidePhoto.objects.filter(
ride=obj, is_approved=True, image__isnull=False
)
RidePhoto.objects.filter(ride=obj, is_approved=True, image__isnull=False)
.order_by("-created_at")
.first()
)
@@ -451,7 +423,7 @@ class RideImageSettingsInputSerializer(serializers.Serializer):
# The ride will be validated in the view
return value
except RidePhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
raise serializers.ValidationError("Photo not found") from None
return value
def validate_card_image_id(self, value):
@@ -464,7 +436,7 @@ class RideImageSettingsInputSerializer(serializers.Serializer):
# The ride will be validated in the view
return value
except RidePhoto.DoesNotExist:
raise serializers.ValidationError("Photo not found")
raise serializers.ValidationError("Photo not found") from None
return value
@@ -474,9 +446,7 @@ class RideCreateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices())
status = serializers.ChoiceField(
choices=ModelChoices.get_ride_status_choices(), default="OPERATING"
)
status = serializers.ChoiceField(choices=ModelChoices.get_ride_status_choices(), default="OPERATING")
# Required park
park_id = serializers.IntegerField()
@@ -490,18 +460,10 @@ class RideCreateInputSerializer(serializers.Serializer):
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
)
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)
@@ -517,18 +479,14 @@ class RideCreateInputSerializer(serializers.Serializer):
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"
)
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"
)
raise serializers.ValidationError("Minimum height cannot be greater than maximum height")
# Park area validation when park changes
park_id = attrs.get("park_id")
@@ -537,6 +495,7 @@ class RideCreateInputSerializer(serializers.Serializer):
if park_id and park_area_id:
try:
from apps.parks.models import ParkArea
park_area = ParkArea.objects.get(id=park_area_id)
if park_area.park_id != park_id:
raise serializers.ValidationError(
@@ -554,12 +513,8 @@ class RideUpdateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
category = serializers.ChoiceField(
choices=ModelChoices.get_ride_category_choices(), required=False
)
status = serializers.ChoiceField(
choices=ModelChoices.get_ride_status_choices(), required=False
)
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices(), required=False)
status = serializers.ChoiceField(choices=ModelChoices.get_ride_status_choices(), required=False)
post_closing_status = serializers.ChoiceField(
choices=ModelChoices.get_ride_post_closing_choices(),
required=False,
@@ -576,18 +531,10 @@ class RideUpdateInputSerializer(serializers.Serializer):
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
)
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)
@@ -603,18 +550,14 @@ class RideUpdateInputSerializer(serializers.Serializer):
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"
)
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"
)
raise serializers.ValidationError("Minimum height cannot be greater than maximum height")
return attrs
@@ -626,9 +569,7 @@ class RideFilterInputSerializer(serializers.Serializer):
search = serializers.CharField(required=False, allow_blank=True)
# Category filter
category = serializers.MultipleChoiceField(
choices=ModelChoices.get_ride_category_choices(), required=False
)
category = serializers.MultipleChoiceField(choices=ModelChoices.get_ride_category_choices(), required=False)
# Status filter
status = serializers.MultipleChoiceField(
@@ -707,33 +648,16 @@ 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
)
height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, allow_null=True)
length_ft = serializers.DecimalField(max_digits=7, decimal_places=2, allow_null=True)
speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, allow_null=True)
inversions = serializers.IntegerField()
ride_time_seconds = serializers.IntegerField(allow_null=True)
track_type = serializers.CharField()
track_material = RichChoiceFieldSerializer(
choice_group="track_materials",
domain="rides"
)
roller_coaster_type = RichChoiceFieldSerializer(
choice_group="coaster_types",
domain="rides"
)
max_drop_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
propulsion_system = RichChoiceFieldSerializer(
choice_group="propulsion_systems",
domain="rides"
)
track_material = RichChoiceFieldSerializer(choice_group="track_materials", domain="rides")
roller_coaster_type = RichChoiceFieldSerializer(choice_group="coaster_types", domain="rides")
max_drop_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, allow_null=True)
propulsion_system = RichChoiceFieldSerializer(choice_group="propulsion_systems", domain="rides")
train_style = serializers.CharField()
trains_count = serializers.IntegerField(allow_null=True)
cars_per_train = serializers.IntegerField(allow_null=True)
@@ -755,30 +679,16 @@ 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
)
height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
length_ft = serializers.DecimalField(max_digits=7, decimal_places=2, required=False, allow_null=True)
speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False, allow_null=True)
inversions = serializers.IntegerField(default=0)
ride_time_seconds = serializers.IntegerField(required=False, allow_null=True)
track_type = serializers.CharField(max_length=255, allow_blank=True, default="")
track_material = serializers.ChoiceField(
choices=ModelChoices.get_coaster_track_choices(), default="STEEL"
)
roller_coaster_type = serializers.ChoiceField(
choices=ModelChoices.get_coaster_type_choices(), default="SITDOWN"
)
max_drop_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
propulsion_system = serializers.ChoiceField(
choices=ModelChoices.get_propulsion_system_choices(), default="CHAIN"
)
track_material = serializers.ChoiceField(choices=ModelChoices.get_coaster_track_choices(), default="STEEL")
roller_coaster_type = serializers.ChoiceField(choices=ModelChoices.get_coaster_type_choices(), default="SITDOWN")
max_drop_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
propulsion_system = serializers.ChoiceField(choices=ModelChoices.get_propulsion_system_choices(), default="CHAIN")
train_style = serializers.CharField(max_length=255, allow_blank=True, default="")
trains_count = serializers.IntegerField(required=False, allow_null=True)
cars_per_train = serializers.IntegerField(required=False, allow_null=True)
@@ -788,33 +698,17 @@ class RollerCoasterStatsCreateInputSerializer(serializers.Serializer):
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
)
height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
length_ft = serializers.DecimalField(max_digits=7, decimal_places=2, required=False, allow_null=True)
speed_mph = serializers.DecimalField(max_digits=5, decimal_places=2, required=False, allow_null=True)
inversions = serializers.IntegerField(required=False)
ride_time_seconds = serializers.IntegerField(required=False, allow_null=True)
track_type = serializers.CharField(max_length=255, allow_blank=True, required=False)
track_material = serializers.ChoiceField(
choices=ModelChoices.get_coaster_track_choices(), required=False
)
roller_coaster_type = serializers.ChoiceField(
choices=ModelChoices.get_coaster_type_choices(), required=False
)
max_drop_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, required=False, allow_null=True
)
propulsion_system = serializers.ChoiceField(
choices=ModelChoices.get_propulsion_system_choices(), required=False
)
train_style = serializers.CharField(
max_length=255, allow_blank=True, required=False
)
track_material = serializers.ChoiceField(choices=ModelChoices.get_coaster_track_choices(), required=False)
roller_coaster_type = serializers.ChoiceField(choices=ModelChoices.get_coaster_type_choices(), required=False)
max_drop_height_ft = serializers.DecimalField(max_digits=6, decimal_places=2, required=False, allow_null=True)
propulsion_system = serializers.ChoiceField(choices=ModelChoices.get_propulsion_system_choices(), required=False)
train_style = serializers.CharField(max_length=255, allow_blank=True, required=False)
trains_count = serializers.IntegerField(required=False, allow_null=True)
cars_per_train = serializers.IntegerField(required=False, allow_null=True)
seats_per_car = serializers.IntegerField(required=False, allow_null=True)

View File

@@ -12,9 +12,7 @@ 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
)
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)
@@ -87,9 +85,7 @@ class RidePhotoUpdateInputSerializer(serializers.ModelSerializer):
class RidePhotoListOutputSerializer(serializers.ModelSerializer):
"""Simplified output serializer for ride photo lists."""
uploaded_by_username = serializers.CharField(
source="uploaded_by.username", read_only=True
)
uploaded_by_username = serializers.CharField(source="uploaded_by.username", read_only=True)
class Meta:
model = RidePhoto
@@ -109,12 +105,8 @@ class RidePhotoListOutputSerializer(serializers.ModelSerializer):
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"
)
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):
@@ -125,9 +117,7 @@ class RidePhotoStatsOutputSerializer(serializers.Serializer):
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"
)
by_type = serializers.DictField(child=serializers.IntegerField(), help_text="Photo counts by type")
class RidePhotoTypeFilterSerializer(serializers.Serializer):

View File

@@ -19,9 +19,7 @@ class EntitySearchInputSerializer(serializers.Serializer):
query = serializers.CharField(max_length=255, help_text="Search query string")
entity_types = serializers.ListField(
child=serializers.ChoiceField(
choices=ModelChoices.get_entity_type_choices()
),
child=serializers.ChoiceField(choices=ModelChoices.get_entity_type_choices()),
required=False,
help_text="Types of entities to search for",
)
@@ -39,17 +37,12 @@ class EntitySearchResultSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
type = RichChoiceFieldSerializer(
choice_group="entity_types",
domain="core"
)
type = RichChoiceFieldSerializer(choice_group="entity_types", domain="core")
description = serializers.CharField()
relevance_score = serializers.FloatField()
# Context-specific info — renamed to avoid overriding Serializer.context
context_info = serializers.JSONField(
help_text="Additional context based on entity type"
)
context_info = serializers.JSONField(help_text="Additional context based on entity type")
class EntitySearchOutputSerializer(serializers.Serializer):

View File

@@ -39,9 +39,7 @@ class SimpleHealthOutputSerializer(serializers.Serializer):
status = serializers.CharField(help_text="Simple health status")
timestamp = serializers.DateTimeField(help_text="Timestamp of health check")
error = serializers.CharField(
required=False, help_text="Error message if unhealthy"
)
error = serializers.CharField(required=False, help_text="Error message if unhealthy")
# === EMAIL SERVICE SERIALIZERS ===
@@ -151,7 +149,7 @@ class ModerationSubmissionSerializer(serializers.Serializer):
("PHOTO", "Photo Submission"),
("REVIEW", "Review Submission"),
],
help_text="Type of submission"
help_text="Type of submission",
)
content_type = serializers.CharField(help_text="Content type being modified")
object_id = serializers.IntegerField(help_text="ID of object being modified")
@@ -221,9 +219,7 @@ class RoadtripOutputSerializer(serializers.Serializer):
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())
)
route_coordinates = serializers.ListField(child=serializers.ListField(child=serializers.FloatField()))
created_at = serializers.DateTimeField()

View File

@@ -25,21 +25,13 @@ class FilterOptionSerializer(serializers.Serializer):
selected?: boolean;
}
"""
value = serializers.CharField(
help_text="The actual value used for filtering"
)
label = serializers.CharField(
help_text="Human-readable display label"
)
value = serializers.CharField(help_text="The actual value used for filtering")
label = serializers.CharField(help_text="Human-readable display label")
count = serializers.IntegerField(
required=False,
allow_null=True,
help_text="Number of items matching this filter option"
)
selected = serializers.BooleanField(
default=False,
help_text="Whether this option is currently selected"
required=False, allow_null=True, help_text="Number of items matching this filter option"
)
selected = serializers.BooleanField(default=False, help_text="Whether this option is currently selected")
class FilterRangeSerializer(serializers.Serializer):
@@ -54,22 +46,12 @@ class FilterRangeSerializer(serializers.Serializer):
unit?: string;
}
"""
min = serializers.FloatField(
allow_null=True,
help_text="Minimum value for the range"
)
max = serializers.FloatField(
allow_null=True,
help_text="Maximum value for the range"
)
step = serializers.FloatField(
default=1.0,
help_text="Step size for range inputs"
)
min = serializers.FloatField(allow_null=True, help_text="Minimum value for the range")
max = serializers.FloatField(allow_null=True, help_text="Maximum value for the range")
step = serializers.FloatField(default=1.0, help_text="Step size for range inputs")
unit = serializers.CharField(
required=False,
allow_null=True,
help_text="Unit of measurement (e.g., 'feet', 'mph', 'stars')"
required=False, allow_null=True, help_text="Unit of measurement (e.g., 'feet', 'mph', 'stars')"
)
@@ -84,15 +66,10 @@ class BooleanFilterSerializer(serializers.Serializer):
description: string;
}
"""
key = serializers.CharField(
help_text="The filter parameter key"
)
label = serializers.CharField(
help_text="Human-readable label for the filter"
)
description = serializers.CharField(
help_text="Description of what this filter does"
)
key = serializers.CharField(help_text="The filter parameter key")
label = serializers.CharField(help_text="Human-readable label for the filter")
description = serializers.CharField(help_text="Description of what this filter does")
class OrderingOptionSerializer(serializers.Serializer):
@@ -105,12 +82,9 @@ class OrderingOptionSerializer(serializers.Serializer):
label: string;
}
"""
value = serializers.CharField(
help_text="The ordering parameter value"
)
label = serializers.CharField(
help_text="Human-readable label for the ordering option"
)
value = serializers.CharField(help_text="The ordering parameter value")
label = serializers.CharField(help_text="Human-readable label for the ordering option")
class StandardizedFilterMetadataSerializer(serializers.Serializer):
@@ -120,27 +94,16 @@ class StandardizedFilterMetadataSerializer(serializers.Serializer):
This serializer ensures all filter metadata responses follow the same structure
that the frontend expects, preventing runtime type errors.
"""
categorical = serializers.DictField(
child=FilterOptionSerializer(many=True),
help_text="Categorical filter options with value/label/count structure"
child=FilterOptionSerializer(many=True), help_text="Categorical filter options with value/label/count structure"
)
ranges = serializers.DictField(
child=FilterRangeSerializer(),
help_text="Range filter metadata with min/max/step/unit"
)
total_count = serializers.IntegerField(
help_text="Total number of items in the filtered dataset"
)
ordering_options = FilterOptionSerializer(
many=True,
required=False,
help_text="Available ordering options"
)
boolean_filters = BooleanFilterSerializer(
many=True,
required=False,
help_text="Available boolean filter options"
child=FilterRangeSerializer(), help_text="Range filter metadata with min/max/step/unit"
)
total_count = serializers.IntegerField(help_text="Total number of items in the filtered dataset")
ordering_options = FilterOptionSerializer(many=True, required=False, help_text="Available ordering options")
boolean_filters = BooleanFilterSerializer(many=True, required=False, help_text="Available boolean filter options")
class PaginationMetadataSerializer(serializers.Serializer):
@@ -157,28 +120,13 @@ class PaginationMetadataSerializer(serializers.Serializer):
total_pages: number;
}
"""
count = serializers.IntegerField(
help_text="Total number of items across all pages"
)
next = serializers.URLField(
allow_null=True,
required=False,
help_text="URL for the next page of results"
)
previous = serializers.URLField(
allow_null=True,
required=False,
help_text="URL for the previous page of results"
)
page_size = serializers.IntegerField(
help_text="Number of items per page"
)
current_page = serializers.IntegerField(
help_text="Current page number (1-indexed)"
)
total_pages = serializers.IntegerField(
help_text="Total number of pages"
)
count = serializers.IntegerField(help_text="Total number of items across all pages")
next = serializers.URLField(allow_null=True, required=False, help_text="URL for the next page of results")
previous = serializers.URLField(allow_null=True, required=False, help_text="URL for the previous page of results")
page_size = serializers.IntegerField(help_text="Number of items per page")
current_page = serializers.IntegerField(help_text="Current page number (1-indexed)")
total_pages = serializers.IntegerField(help_text="Total number of pages")
class ApiResponseSerializer(serializers.Serializer):
@@ -193,22 +141,14 @@ class ApiResponseSerializer(serializers.Serializer):
errors?: ValidationError;
}
"""
success = serializers.BooleanField(
help_text="Whether the request was successful"
)
success = serializers.BooleanField(help_text="Whether the request was successful")
response_data = serializers.JSONField(
required=False,
help_text="Response data (structure varies by endpoint)",
source='data'
)
message = serializers.CharField(
required=False,
help_text="Human-readable message about the operation"
required=False, help_text="Response data (structure varies by endpoint)", source="data"
)
message = serializers.CharField(required=False, help_text="Human-readable message about the operation")
response_errors = serializers.DictField(
required=False,
help_text="Validation errors (field -> error messages)",
source='errors'
required=False, help_text="Validation errors (field -> error messages)", source="errors"
)
@@ -228,18 +168,11 @@ class ErrorResponseSerializer(serializers.Serializer):
data: null;
}
"""
status = serializers.CharField(
default="error",
help_text="Response status indicator"
)
error = serializers.DictField(
help_text="Error details"
)
status = serializers.CharField(default="error", help_text="Response status indicator")
error = serializers.DictField(help_text="Error details")
response_data = serializers.JSONField(
default=None,
allow_null=True,
help_text="Always null for error responses",
source='data'
default=None, allow_null=True, help_text="Always null for error responses", source="data"
)
@@ -257,32 +190,13 @@ class LocationSerializer(serializers.Serializer):
longitude?: number;
}
"""
city = serializers.CharField(
help_text="City name"
)
state = serializers.CharField(
required=False,
allow_null=True,
help_text="State/province name"
)
country = serializers.CharField(
help_text="Country name"
)
address = serializers.CharField(
required=False,
allow_null=True,
help_text="Street address"
)
latitude = serializers.FloatField(
required=False,
allow_null=True,
help_text="Latitude coordinate"
)
longitude = serializers.FloatField(
required=False,
allow_null=True,
help_text="Longitude coordinate"
)
city = serializers.CharField(help_text="City name")
state = serializers.CharField(required=False, allow_null=True, help_text="State/province name")
country = serializers.CharField(help_text="Country name")
address = serializers.CharField(required=False, allow_null=True, help_text="Street address")
latitude = serializers.FloatField(required=False, allow_null=True, help_text="Latitude coordinate")
longitude = serializers.FloatField(required=False, allow_null=True, help_text="Longitude coordinate")
# Alias for backward compatibility
@@ -301,24 +215,15 @@ class CompanyOutputSerializer(serializers.Serializer):
roles?: string[];
}
"""
id = serializers.IntegerField(
help_text="Company ID"
)
name = serializers.CharField(
help_text="Company name"
)
slug = serializers.SlugField(
help_text="URL-friendly identifier"
)
id = serializers.IntegerField(help_text="Company ID")
name = serializers.CharField(help_text="Company name")
slug = serializers.SlugField(help_text="URL-friendly identifier")
roles = serializers.ListField(
child=serializers.CharField(),
required=False,
help_text="Company roles (manufacturer, operator, etc.)"
child=serializers.CharField(), required=False, help_text="Company roles (manufacturer, operator, etc.)"
)
class ModelChoices:
"""
Utility class to provide model choices for serializers using Rich Choice Objects.
@@ -331,6 +236,7 @@ class ModelChoices:
def get_park_status_choices():
"""Get park status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("statuses", "parks")
return [(choice.value, choice.label) for choice in choices]
@@ -338,6 +244,7 @@ class ModelChoices:
def get_ride_status_choices():
"""Get ride status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("statuses", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -345,6 +252,7 @@ class ModelChoices:
def get_company_role_choices():
"""Get company role choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
# Get rides domain company roles (MANUFACTURER, DESIGNER)
rides_choices = get_choices("company_roles", "rides")
# Get parks domain company roles (OPERATOR, PROPERTY_OWNER)
@@ -356,6 +264,7 @@ class ModelChoices:
def get_ride_category_choices():
"""Get ride category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -363,6 +272,7 @@ class ModelChoices:
def get_ride_post_closing_choices():
"""Get ride post-closing status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("post_closing_statuses", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -370,6 +280,7 @@ class ModelChoices:
def get_coaster_track_choices():
"""Get coaster track material choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("track_materials", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -377,6 +288,7 @@ class ModelChoices:
def get_coaster_type_choices():
"""Get coaster type choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("coaster_types", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -384,6 +296,7 @@ class ModelChoices:
def get_launch_choices():
"""Get launch system choices from Rich Choice registry (legacy method)."""
from apps.core.choices.registry import get_choices
choices = get_choices("propulsion_systems", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -391,6 +304,7 @@ class ModelChoices:
def get_propulsion_system_choices():
"""Get propulsion system choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("propulsion_systems", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -398,6 +312,7 @@ class ModelChoices:
def get_photo_type_choices():
"""Get photo type choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("photo_types", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -405,6 +320,7 @@ class ModelChoices:
def get_spec_category_choices():
"""Get technical specification category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("spec_categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -412,6 +328,7 @@ class ModelChoices:
def get_technical_spec_category_choices():
"""Get technical specification category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("spec_categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -419,6 +336,7 @@ class ModelChoices:
def get_target_market_choices():
"""Get target market choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("target_markets", "rides")
return [(choice.value, choice.label) for choice in choices]
@@ -426,6 +344,7 @@ class ModelChoices:
def get_entity_type_choices():
"""Get entity type choices for search functionality."""
from apps.core.choices.registry import get_choices
choices = get_choices("entity_types", "core")
return [(choice.value, choice.label) for choice in choices]
@@ -433,6 +352,7 @@ class ModelChoices:
def get_health_status_choices():
"""Get health check status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("health_statuses", "core")
return [(choice.value, choice.label) for choice in choices]
@@ -440,6 +360,7 @@ class ModelChoices:
def get_simple_health_status_choices():
"""Get simple health check status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("simple_health_statuses", "core")
return [(choice.value, choice.label) for choice in choices]
@@ -455,15 +376,10 @@ class EntityReferenceSerializer(serializers.Serializer):
slug: string;
}
"""
id = serializers.IntegerField(
help_text="Unique identifier"
)
name = serializers.CharField(
help_text="Display name"
)
slug = serializers.SlugField(
help_text="URL-friendly identifier"
)
id = serializers.IntegerField(help_text="Unique identifier")
name = serializers.CharField(help_text="Display name")
slug = serializers.SlugField(help_text="URL-friendly identifier")
class ImageVariantsSerializer(serializers.Serializer):
@@ -478,19 +394,11 @@ class ImageVariantsSerializer(serializers.Serializer):
avatar?: string;
}
"""
thumbnail = serializers.URLField(
help_text="Thumbnail size image URL"
)
medium = serializers.URLField(
help_text="Medium size image URL"
)
large = serializers.URLField(
help_text="Large size image URL"
)
avatar = serializers.URLField(
required=False,
help_text="Avatar size image URL (for user avatars)"
)
thumbnail = serializers.URLField(help_text="Thumbnail size image URL")
medium = serializers.URLField(help_text="Medium size image URL")
large = serializers.URLField(help_text="Large size image URL")
avatar = serializers.URLField(required=False, help_text="Avatar size image URL (for user avatars)")
class PhotoSerializer(serializers.Serializer):
@@ -509,39 +417,15 @@ class PhotoSerializer(serializers.Serializer):
uploaded_at?: string;
}
"""
id = serializers.IntegerField(
help_text="Photo ID"
)
image_variants = ImageVariantsSerializer(
help_text="Available image size variants"
)
alt_text = serializers.CharField(
required=False,
allow_null=True,
help_text="Alternative text for accessibility"
)
image_url = serializers.URLField(
required=False,
help_text="Primary image URL (for compatibility)"
)
caption = serializers.CharField(
required=False,
allow_null=True,
help_text="Photo caption"
)
photo_type = serializers.CharField(
required=False,
allow_null=True,
help_text="Type/category of photo"
)
uploaded_by = EntityReferenceSerializer(
required=False,
help_text="User who uploaded the photo"
)
uploaded_at = serializers.DateTimeField(
required=False,
help_text="When the photo was uploaded"
)
id = serializers.IntegerField(help_text="Photo ID")
image_variants = ImageVariantsSerializer(help_text="Available image size variants")
alt_text = serializers.CharField(required=False, allow_null=True, help_text="Alternative text for accessibility")
image_url = serializers.URLField(required=False, help_text="Primary image URL (for compatibility)")
caption = serializers.CharField(required=False, allow_null=True, help_text="Photo caption")
photo_type = serializers.CharField(required=False, allow_null=True, help_text="Type/category of photo")
uploaded_by = EntityReferenceSerializer(required=False, help_text="User who uploaded the photo")
uploaded_at = serializers.DateTimeField(required=False, help_text="When the photo was uploaded")
class UserInfoSerializer(serializers.Serializer):
@@ -556,20 +440,11 @@ class UserInfoSerializer(serializers.Serializer):
avatar_url?: string;
}
"""
id = serializers.IntegerField(
help_text="User ID"
)
username = serializers.CharField(
help_text="Username"
)
display_name = serializers.CharField(
help_text="Display name"
)
avatar_url = serializers.URLField(
required=False,
allow_null=True,
help_text="User avatar URL"
)
id = serializers.IntegerField(help_text="User ID")
username = serializers.CharField(help_text="Username")
display_name = serializers.CharField(help_text="Display name")
avatar_url = serializers.URLField(required=False, allow_null=True, help_text="User avatar URL")
def validate_filter_metadata_contract(data: dict[str, Any]) -> dict[str, Any]:
@@ -613,27 +488,22 @@ def ensure_filter_option_format(options: list[Any]) -> list[dict[str, Any]]:
if isinstance(option, dict):
# Already in correct format or close to it
standardized_option = {
'value': str(option.get('value', option.get('id', ''))),
'label': option.get('label', option.get('name', str(option.get('value', '')))),
'count': option.get('count'),
'selected': option.get('selected', False)
"value": str(option.get("value", option.get("id", ""))),
"label": option.get("label", option.get("name", str(option.get("value", "")))),
"count": option.get("count"),
"selected": option.get("selected", False),
}
elif hasattr(option, 'value') and hasattr(option, 'label'):
elif hasattr(option, "value") and hasattr(option, "label"):
# RichChoice object format
standardized_option = {
'value': str(option.value),
'label': str(option.label),
'count': None,
'selected': False
"value": str(option.value),
"label": str(option.label),
"count": None,
"selected": False,
}
else:
# Simple value - use as both value and label
standardized_option = {
'value': str(option),
'label': str(option),
'count': None,
'selected': False
}
standardized_option = {"value": str(option), "label": str(option), "count": None, "selected": False}
standardized.append(standardized_option)
@@ -651,8 +521,8 @@ def ensure_range_format(range_data: dict[str, Any]) -> dict[str, Any]:
Range data in standard format
"""
return {
'min': range_data.get('min'),
'max': range_data.get('max'),
'step': range_data.get('step', 1.0),
'unit': range_data.get('unit')
"min": range_data.get("min"),
"max": range_data.get("max"),
"step": range_data.get("step", 1.0),
"unit": range_data.get("unit"),
}

View File

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