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