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

@@ -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"),
}