Add comprehensive API documentation for ThrillWiki integration and features

- Introduced Next.js integration guide for ThrillWiki API, detailing authentication, core domain APIs, data structures, and implementation patterns.
- Documented the migration to Rich Choice Objects, highlighting changes for frontend developers and enhanced metadata availability.
- Fixed the missing `get_by_slug` method in the Ride model, ensuring proper functionality of ride detail endpoints.
- Created a test script to verify manufacturer syncing with ride models, ensuring data integrity across related models.
This commit is contained in:
pacnpal
2025-09-16 11:29:17 -04:00
parent 61d73a2147
commit c2c26cfd1d
98 changed files with 11476 additions and 4803 deletions

View File

@@ -0,0 +1,12 @@
"""
Rides Django App
This app handles all ride-related functionality including ride models,
companies, rankings, and search functionality.
"""
# Import choices to ensure they are registered with the global registry
from . import choices
# Ensure choices are registered on app startup
__all__ = ['choices']

View File

@@ -197,9 +197,11 @@ class RideAdmin(admin.ModelAdmin):
@admin.display(description="Category")
def category_display(self, obj):
"""Display category with full name"""
return dict(obj._meta.get_field("category").choices).get(
obj.category, obj.category
)
choices_dict = dict(obj._meta.get_field("category").choices)
if obj.category in choices_dict:
return choices_dict[obj.category]
else:
raise ValueError(f"Unknown category: {obj.category}")
@admin.register(RideModel)
@@ -240,9 +242,11 @@ class RideModelAdmin(admin.ModelAdmin):
@admin.display(description="Category")
def category_display(self, obj):
"""Display category with full name"""
return dict(obj._meta.get_field("category").choices).get(
obj.category, obj.category
)
choices_dict = dict(obj._meta.get_field("category").choices)
if obj.category in choices_dict:
return choices_dict[obj.category]
else:
raise ValueError(f"Unknown category: {obj.category}")
@admin.display(description="Installations")
def ride_count(self, obj):

View File

@@ -0,0 +1,804 @@
"""
Rich Choice Objects for Rides Domain
This module defines all choice objects for the rides domain, replacing
the legacy tuple-based choices with rich choice objects.
"""
from apps.core.choices import RichChoice, ChoiceCategory
from apps.core.choices.registry import register_choices
# Ride Category Choices
RIDE_CATEGORIES = [
RichChoice(
value="RC",
label="Roller Coaster",
description="Thrill rides with tracks featuring hills, loops, and high speeds",
metadata={
'color': 'red',
'icon': 'roller-coaster',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 1
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="DR",
label="Dark Ride",
description="Indoor rides with themed environments and storytelling",
metadata={
'color': 'purple',
'icon': 'dark-ride',
'css_class': 'bg-purple-100 text-purple-800',
'sort_order': 2
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="FR",
label="Flat Ride",
description="Rides that move along a generally flat plane with spinning, swinging, or bouncing motions",
metadata={
'color': 'blue',
'icon': 'flat-ride',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 3
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="WR",
label="Water Ride",
description="Rides that incorporate water elements like splashing, floating, or getting wet",
metadata={
'color': 'cyan',
'icon': 'water-ride',
'css_class': 'bg-cyan-100 text-cyan-800',
'sort_order': 4
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="TR",
label="Transport Ride",
description="Rides primarily designed for transportation around the park",
metadata={
'color': 'green',
'icon': 'transport',
'css_class': 'bg-green-100 text-green-800',
'sort_order': 5
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="OT",
label="Other",
description="Rides that don't fit into standard categories",
metadata={
'color': 'gray',
'icon': 'other',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 6
},
category=ChoiceCategory.CLASSIFICATION
),
]
# Ride Status Choices
RIDE_STATUSES = [
RichChoice(
value="OPERATING",
label="Operating",
description="Ride is currently open and operating normally",
metadata={
'color': 'green',
'icon': 'check-circle',
'css_class': 'bg-green-100 text-green-800',
'sort_order': 1
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="CLOSED_TEMP",
label="Temporarily Closed",
description="Ride is temporarily closed for maintenance, weather, or other short-term reasons",
metadata={
'color': 'yellow',
'icon': 'pause-circle',
'css_class': 'bg-yellow-100 text-yellow-800',
'sort_order': 2
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="SBNO",
label="Standing But Not Operating",
description="Ride structure remains but is not currently operating",
metadata={
'color': 'orange',
'icon': 'stop-circle',
'css_class': 'bg-orange-100 text-orange-800',
'sort_order': 3
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="CLOSING",
label="Closing",
description="Ride is scheduled to close permanently",
metadata={
'color': 'red',
'icon': 'x-circle',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 4
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="CLOSED_PERM",
label="Permanently Closed",
description="Ride has been permanently closed and will not reopen",
metadata={
'color': 'red',
'icon': 'x-circle',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 5
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="UNDER_CONSTRUCTION",
label="Under Construction",
description="Ride is currently being built or undergoing major renovation",
metadata={
'color': 'blue',
'icon': 'tool',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 6
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="DEMOLISHED",
label="Demolished",
description="Ride has been completely removed and demolished",
metadata={
'color': 'gray',
'icon': 'trash',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 7
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="RELOCATED",
label="Relocated",
description="Ride has been moved to a different location",
metadata={
'color': 'purple',
'icon': 'arrow-right',
'css_class': 'bg-purple-100 text-purple-800',
'sort_order': 8
},
category=ChoiceCategory.STATUS
),
]
# Post-Closing Status Choices
POST_CLOSING_STATUSES = [
RichChoice(
value="SBNO",
label="Standing But Not Operating",
description="Ride structure remains but is not operating after closure",
metadata={
'color': 'orange',
'icon': 'stop-circle',
'css_class': 'bg-orange-100 text-orange-800',
'sort_order': 1
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="CLOSED_PERM",
label="Permanently Closed",
description="Ride has been permanently closed after the closing date",
metadata={
'color': 'red',
'icon': 'x-circle',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 2
},
category=ChoiceCategory.STATUS
),
]
# Roller Coaster Track Material Choices
TRACK_MATERIALS = [
RichChoice(
value="STEEL",
label="Steel",
description="Modern steel track construction providing smooth rides and complex layouts",
metadata={
'color': 'gray',
'icon': 'steel',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 1
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="WOOD",
label="Wood",
description="Traditional wooden track construction providing classic coaster experience",
metadata={
'color': 'amber',
'icon': 'wood',
'css_class': 'bg-amber-100 text-amber-800',
'sort_order': 2
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="HYBRID",
label="Hybrid",
description="Combination of steel and wooden construction elements",
metadata={
'color': 'orange',
'icon': 'hybrid',
'css_class': 'bg-orange-100 text-orange-800',
'sort_order': 3
},
category=ChoiceCategory.TECHNICAL
),
]
# Roller Coaster Type Choices
COASTER_TYPES = [
RichChoice(
value="SITDOWN",
label="Sit Down",
description="Traditional seated roller coaster with riders sitting upright",
metadata={
'color': 'blue',
'icon': 'sitdown',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 1
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="INVERTED",
label="Inverted",
description="Coaster where riders' feet dangle freely below the track",
metadata={
'color': 'purple',
'icon': 'inverted',
'css_class': 'bg-purple-100 text-purple-800',
'sort_order': 2
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="FLYING",
label="Flying",
description="Riders lie face-down in a flying position",
metadata={
'color': 'sky',
'icon': 'flying',
'css_class': 'bg-sky-100 text-sky-800',
'sort_order': 3
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="STANDUP",
label="Stand Up",
description="Riders stand upright during the ride",
metadata={
'color': 'green',
'icon': 'standup',
'css_class': 'bg-green-100 text-green-800',
'sort_order': 4
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="WING",
label="Wing",
description="Riders sit on either side of the track with nothing above or below",
metadata={
'color': 'indigo',
'icon': 'wing',
'css_class': 'bg-indigo-100 text-indigo-800',
'sort_order': 5
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="DIVE",
label="Dive",
description="Features a vertical or near-vertical drop as the main element",
metadata={
'color': 'red',
'icon': 'dive',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 6
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="FAMILY",
label="Family",
description="Designed for riders of all ages with moderate thrills",
metadata={
'color': 'emerald',
'icon': 'family',
'css_class': 'bg-emerald-100 text-emerald-800',
'sort_order': 7
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="WILD_MOUSE",
label="Wild Mouse",
description="Compact coaster with sharp turns and sudden drops",
metadata={
'color': 'yellow',
'icon': 'mouse',
'css_class': 'bg-yellow-100 text-yellow-800',
'sort_order': 8
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="SPINNING",
label="Spinning",
description="Cars rotate freely during the ride",
metadata={
'color': 'pink',
'icon': 'spinning',
'css_class': 'bg-pink-100 text-pink-800',
'sort_order': 9
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="FOURTH_DIMENSION",
label="4th Dimension",
description="Seats rotate independently of the track direction",
metadata={
'color': 'violet',
'icon': '4d',
'css_class': 'bg-violet-100 text-violet-800',
'sort_order': 10
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="OTHER",
label="Other",
description="Coaster type that doesn't fit standard classifications",
metadata={
'color': 'gray',
'icon': 'other',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 11
},
category=ChoiceCategory.CLASSIFICATION
),
]
# Launch System Choices
LAUNCH_SYSTEMS = [
RichChoice(
value="CHAIN",
label="Chain Lift",
description="Traditional chain lift system to pull trains up the lift hill",
metadata={
'color': 'gray',
'icon': 'chain',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 1
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="LSM",
label="LSM Launch",
description="Linear Synchronous Motor launch system using magnetic propulsion",
metadata={
'color': 'blue',
'icon': 'lightning',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 2
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="HYDRAULIC",
label="Hydraulic Launch",
description="High-pressure hydraulic launch system for rapid acceleration",
metadata={
'color': 'red',
'icon': 'hydraulic',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 3
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="GRAVITY",
label="Gravity",
description="Uses gravity and momentum without mechanical lift systems",
metadata={
'color': 'green',
'icon': 'gravity',
'css_class': 'bg-green-100 text-green-800',
'sort_order': 4
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="OTHER",
label="Other",
description="Launch system that doesn't fit standard categories",
metadata={
'color': 'gray',
'icon': 'other',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 5
},
category=ChoiceCategory.TECHNICAL
),
]
# Ride Model Target Market Choices
TARGET_MARKETS = [
RichChoice(
value="FAMILY",
label="Family",
description="Designed for families with children, moderate thrills",
metadata={
'color': 'green',
'icon': 'family',
'css_class': 'bg-green-100 text-green-800',
'sort_order': 1
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="THRILL",
label="Thrill",
description="High-intensity rides for thrill seekers",
metadata={
'color': 'red',
'icon': 'thrill',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 2
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="EXTREME",
label="Extreme",
description="Maximum intensity rides for extreme thrill seekers",
metadata={
'color': 'purple',
'icon': 'extreme',
'css_class': 'bg-purple-100 text-purple-800',
'sort_order': 3
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="KIDDIE",
label="Kiddie",
description="Gentle rides designed specifically for young children",
metadata={
'color': 'yellow',
'icon': 'kiddie',
'css_class': 'bg-yellow-100 text-yellow-800',
'sort_order': 4
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="ALL_AGES",
label="All Ages",
description="Suitable for riders of all ages and thrill preferences",
metadata={
'color': 'blue',
'icon': 'all-ages',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 5
},
category=ChoiceCategory.CLASSIFICATION
),
]
# Ride Model Photo Type Choices
PHOTO_TYPES = [
RichChoice(
value="PROMOTIONAL",
label="Promotional",
description="Marketing and promotional photos of the ride model",
metadata={
'color': 'blue',
'icon': 'camera',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 1
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="TECHNICAL",
label="Technical Drawing",
description="Technical drawings and engineering diagrams",
metadata={
'color': 'gray',
'icon': 'blueprint',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 2
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="INSTALLATION",
label="Installation Example",
description="Photos of actual installations of this ride model",
metadata={
'color': 'green',
'icon': 'installation',
'css_class': 'bg-green-100 text-green-800',
'sort_order': 3
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="RENDERING",
label="3D Rendering",
description="Computer-generated 3D renderings of the ride model",
metadata={
'color': 'purple',
'icon': 'cube',
'css_class': 'bg-purple-100 text-purple-800',
'sort_order': 4
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="CATALOG",
label="Catalog Image",
description="Official catalog and brochure images",
metadata={
'color': 'orange',
'icon': 'catalog',
'css_class': 'bg-orange-100 text-orange-800',
'sort_order': 5
},
category=ChoiceCategory.CLASSIFICATION
),
]
# Technical Specification Category Choices
SPEC_CATEGORIES = [
RichChoice(
value="DIMENSIONS",
label="Dimensions",
description="Physical dimensions and measurements",
metadata={
'color': 'blue',
'icon': 'ruler',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 1
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="PERFORMANCE",
label="Performance",
description="Performance specifications and capabilities",
metadata={
'color': 'red',
'icon': 'speedometer',
'css_class': 'bg-red-100 text-red-800',
'sort_order': 2
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="CAPACITY",
label="Capacity",
description="Rider capacity and throughput specifications",
metadata={
'color': 'green',
'icon': 'users',
'css_class': 'bg-green-100 text-green-800',
'sort_order': 3
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="SAFETY",
label="Safety Features",
description="Safety systems and features",
metadata={
'color': 'yellow',
'icon': 'shield',
'css_class': 'bg-yellow-100 text-yellow-800',
'sort_order': 4
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="ELECTRICAL",
label="Electrical Requirements",
description="Power and electrical system requirements",
metadata={
'color': 'purple',
'icon': 'lightning',
'css_class': 'bg-purple-100 text-purple-800',
'sort_order': 5
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="FOUNDATION",
label="Foundation Requirements",
description="Foundation and structural requirements",
metadata={
'color': 'gray',
'icon': 'foundation',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 6
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="MAINTENANCE",
label="Maintenance",
description="Maintenance requirements and procedures",
metadata={
'color': 'orange',
'icon': 'wrench',
'css_class': 'bg-orange-100 text-orange-800',
'sort_order': 7
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="OTHER",
label="Other",
description="Other technical specifications",
metadata={
'color': 'gray',
'icon': 'other',
'css_class': 'bg-gray-100 text-gray-800',
'sort_order': 8
},
category=ChoiceCategory.TECHNICAL
),
]
# Company Role Choices for Rides Domain (MANUFACTURER and DESIGNER only)
RIDES_COMPANY_ROLES = [
RichChoice(
value="MANUFACTURER",
label="Ride Manufacturer",
description="Company that designs and builds ride hardware and systems",
metadata={
'color': 'blue',
'icon': 'factory',
'css_class': 'bg-blue-100 text-blue-800',
'sort_order': 1,
'domain': 'rides',
'permissions': ['manage_ride_models', 'view_manufacturing'],
'url_pattern': '/rides/manufacturers/{slug}/'
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="DESIGNER",
label="Ride Designer",
description="Company that specializes in ride design, layout, and engineering",
metadata={
'color': 'purple',
'icon': 'design',
'css_class': 'bg-purple-100 text-purple-800',
'sort_order': 2,
'domain': 'rides',
'permissions': ['manage_ride_designs', 'view_design_specs'],
'url_pattern': '/rides/designers/{slug}/'
},
category=ChoiceCategory.CLASSIFICATION
),
]
def register_rides_choices():
"""Register all rides domain choices with the global registry"""
register_choices(
name="categories",
choices=RIDE_CATEGORIES,
domain="rides",
description="Ride category classifications",
metadata={'domain': 'rides', 'type': 'category'}
)
register_choices(
name="statuses",
choices=RIDE_STATUSES,
domain="rides",
description="Ride operational status options",
metadata={'domain': 'rides', 'type': 'status'}
)
register_choices(
name="post_closing_statuses",
choices=POST_CLOSING_STATUSES,
domain="rides",
description="Status options after ride closure",
metadata={'domain': 'rides', 'type': 'post_closing_status'}
)
register_choices(
name="track_materials",
choices=TRACK_MATERIALS,
domain="rides",
description="Roller coaster track material types",
metadata={'domain': 'rides', 'type': 'track_material', 'applies_to': 'roller_coasters'}
)
register_choices(
name="coaster_types",
choices=COASTER_TYPES,
domain="rides",
description="Roller coaster type classifications",
metadata={'domain': 'rides', 'type': 'coaster_type', 'applies_to': 'roller_coasters'}
)
register_choices(
name="launch_systems",
choices=LAUNCH_SYSTEMS,
domain="rides",
description="Roller coaster launch and lift systems",
metadata={'domain': 'rides', 'type': 'launch_system', 'applies_to': 'roller_coasters'}
)
register_choices(
name="target_markets",
choices=TARGET_MARKETS,
domain="rides",
description="Target market classifications for ride models",
metadata={'domain': 'rides', 'type': 'target_market', 'applies_to': 'ride_models'}
)
register_choices(
name="photo_types",
choices=PHOTO_TYPES,
domain="rides",
description="Photo type classifications for ride model images",
metadata={'domain': 'rides', 'type': 'photo_type', 'applies_to': 'ride_model_photos'}
)
register_choices(
name="spec_categories",
choices=SPEC_CATEGORIES,
domain="rides",
description="Technical specification category classifications",
metadata={'domain': 'rides', 'type': 'spec_category', 'applies_to': 'ride_model_specs'}
)
register_choices(
name="company_roles",
choices=RIDES_COMPANY_ROLES,
domain="rides",
description="Company role classifications for rides domain (MANUFACTURER and DESIGNER only)",
metadata={'domain': 'rides', 'type': 'company_role'}
)
# Auto-register choices when module is imported
register_rides_choices()

View File

@@ -25,17 +25,21 @@ def get_ride_display_changes(changes: Dict) -> Dict:
# Format specific fields
if field == "status":
from .models import Ride
choices = dict(Ride.STATUS_CHOICES)
old_value = choices.get(old_value, old_value)
new_value = choices.get(new_value, new_value)
from .choices import RIDE_STATUSES
choices = {choice.value: choice.label for choice in RIDE_STATUSES}
if old_value in choices:
old_value = choices[old_value]
if new_value in choices:
new_value = choices[new_value]
elif field == "post_closing_status":
from .models import Ride
choices = dict(Ride.POST_CLOSING_STATUS_CHOICES)
old_value = choices.get(old_value, old_value)
new_value = choices.get(new_value, new_value)
from .choices import POST_CLOSING_STATUSES
choices = {choice.value: choice.label for choice in POST_CLOSING_STATUSES}
if old_value in choices:
old_value = choices[old_value]
if new_value in choices:
new_value = choices[new_value]
display_changes[field_names[field]] = {
"old": old_value,
@@ -61,11 +65,13 @@ def get_ride_model_display_changes(changes: Dict) -> Dict:
# Format category field
if field == "category":
from .models import CATEGORY_CHOICES
from .choices import RIDE_CATEGORIES
choices = dict(CATEGORY_CHOICES)
old_value = choices.get(old_value, old_value)
new_value = choices.get(new_value, new_value)
choices = {choice.value: choice.label for choice in RIDE_CATEGORIES}
if old_value in choices:
old_value = choices[old_value]
if new_value in choices:
new_value = choices[new_value]
display_changes[field_names[field]] = {
"old": old_value,

View File

@@ -93,36 +93,28 @@ class SearchTextForm(BaseFilterForm):
class BasicInfoForm(BaseFilterForm):
"""Form for basic ride information filters."""
CATEGORY_CHOICES = [
("", "All Categories"),
("roller_coaster", "Roller Coaster"),
("water_ride", "Water Ride"),
("flat_ride", "Flat Ride"),
("dark_ride", "Dark Ride"),
("kiddie_ride", "Kiddie Ride"),
("transport", "Transport"),
("show", "Show"),
("other", "Other"),
]
STATUS_CHOICES = [
("", "All Statuses"),
("operating", "Operating"),
("closed_temporary", "Temporarily Closed"),
("closed_permanent", "Permanently Closed"),
("under_construction", "Under Construction"),
("announced", "Announced"),
("rumored", "Rumored"),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Get choices from Rich Choice registry
from apps.core.choices.registry import get_choices
# Get choices - let exceptions propagate if registry fails
category_choices = [(choice.value, choice.label) for choice in get_choices("categories", "rides")]
status_choices = [(choice.value, choice.label) for choice in get_choices("statuses", "rides")]
# Update field choices dynamically
self.fields['category'].choices = category_choices
self.fields['status'].choices = status_choices
category = forms.MultipleChoiceField(
choices=CATEGORY_CHOICES[1:], # Exclude "All Categories"
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(attrs={"class": "grid grid-cols-2 gap-2"}),
)
status = forms.MultipleChoiceField(
choices=STATUS_CHOICES[1:], # Exclude "All Statuses"
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}),
)
@@ -346,35 +338,21 @@ class RelationshipsForm(BaseFilterForm):
class RollerCoasterForm(BaseFilterForm):
"""Form for roller coaster specific filters."""
TRACK_MATERIAL_CHOICES = [
("steel", "Steel"),
("wood", "Wood"),
("hybrid", "Hybrid"),
]
COASTER_TYPE_CHOICES = [
("sit_down", "Sit Down"),
("inverted", "Inverted"),
("floorless", "Floorless"),
("flying", "Flying"),
("suspended", "Suspended"),
("stand_up", "Stand Up"),
("spinning", "Spinning"),
("launched", "Launched"),
("hypercoaster", "Hypercoaster"),
("giga_coaster", "Giga Coaster"),
("strata_coaster", "Strata Coaster"),
]
LAUNCH_TYPE_CHOICES = [
("none", "No Launch"),
("lim", "LIM (Linear Induction Motor)"),
("lsm", "LSM (Linear Synchronous Motor)"),
("hydraulic", "Hydraulic"),
("pneumatic", "Pneumatic"),
("cable", "Cable"),
("flywheel", "Flywheel"),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Get choices from Rich Choice registry
from apps.core.choices.registry import get_choices
# Get choices - let exceptions propagate if registry fails
track_material_choices = [(choice.value, choice.label) for choice in get_choices("track_materials", "rides")]
coaster_type_choices = [(choice.value, choice.label) for choice in get_choices("coaster_types", "rides")]
launch_type_choices = [(choice.value, choice.label) for choice in get_choices("launch_systems", "rides")]
# Update field choices dynamically
self.fields['track_material'].choices = track_material_choices
self.fields['coaster_type'].choices = coaster_type_choices
self.fields['launch_type'].choices = launch_type_choices
height_ft_range = NumberRangeField(
min_val=0, max_val=500, step=1, required=False, label="Height (feet)"
@@ -393,13 +371,13 @@ class RollerCoasterForm(BaseFilterForm):
)
track_material = forms.MultipleChoiceField(
choices=TRACK_MATERIAL_CHOICES,
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}),
)
coaster_type = forms.MultipleChoiceField(
choices=COASTER_TYPE_CHOICES,
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(
attrs={"class": "grid grid-cols-2 gap-2 max-h-48 overflow-y-auto"}
@@ -407,7 +385,7 @@ class RollerCoasterForm(BaseFilterForm):
)
launch_type = forms.MultipleChoiceField(
choices=LAUNCH_TYPE_CHOICES,
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(
attrs={"class": "space-y-2 max-h-48 overflow-y-auto"}
@@ -418,20 +396,29 @@ class RollerCoasterForm(BaseFilterForm):
class CompanyForm(BaseFilterForm):
"""Form for company-related filters."""
ROLE_CHOICES = [
("MANUFACTURER", "Manufacturer"),
("DESIGNER", "Designer"),
("OPERATOR", "Operator"),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Get choices from Rich Choice registry
from apps.core.choices.registry import get_choices
# Get both rides and parks company roles - let exceptions propagate if registry fails
rides_roles = [(choice.value, choice.label) for choice in get_choices("company_roles", "rides")]
parks_roles = [(choice.value, choice.label) for choice in get_choices("company_roles", "parks")]
role_choices = rides_roles + parks_roles
# Update field choices dynamically
self.fields['manufacturer_roles'].choices = role_choices
self.fields['designer_roles'].choices = role_choices
manufacturer_roles = forms.MultipleChoiceField(
choices=ROLE_CHOICES,
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}),
)
designer_roles = forms.MultipleChoiceField(
choices=ROLE_CHOICES,
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}),
)
@@ -444,24 +431,31 @@ class CompanyForm(BaseFilterForm):
class SortingForm(BaseFilterForm):
"""Form for sorting options."""
SORT_CHOICES = [
("relevance", "Relevance"),
("name_asc", "Name (A-Z)"),
("name_desc", "Name (Z-A)"),
("opening_date_asc", "Opening Date (Oldest)"),
("opening_date_desc", "Opening Date (Newest)"),
("rating_asc", "Rating (Lowest)"),
("rating_desc", "Rating (Highest)"),
("height_asc", "Height (Shortest)"),
("height_desc", "Height (Tallest)"),
("speed_asc", "Speed (Slowest)"),
("speed_desc", "Speed (Fastest)"),
("capacity_asc", "Capacity (Lowest)"),
("capacity_desc", "Capacity (Highest)"),
]
# Static sorting choices - these are UI-specific and don't need Rich Choice Objects
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Static sort choices for UI functionality
sort_choices = [
("relevance", "Relevance"),
("name_asc", "Name (A-Z)"),
("name_desc", "Name (Z-A)"),
("opening_date_asc", "Opening Date (Oldest)"),
("opening_date_desc", "Opening Date (Newest)"),
("rating_asc", "Rating (Lowest)"),
("rating_desc", "Rating (Highest)"),
("height_asc", "Height (Shortest)"),
("height_desc", "Height (Tallest)"),
("speed_asc", "Speed (Slowest)"),
("speed_desc", "Speed (Fastest)"),
("capacity_asc", "Capacity (Lowest)"),
("capacity_desc", "Capacity (Highest)"),
]
self.fields['sort_by'].choices = sort_choices
sort_by = forms.ChoiceField(
choices=SORT_CHOICES,
choices=[], # Will be populated in __init__
required=False,
initial="relevance",
widget=forms.Select(

View File

@@ -14,7 +14,7 @@ Index Strategy:
Performance Target: <100ms for most filter combinations
"""
from django.db import migrations, models
from django.db import migrations
class Migration(migrations.Migration):

View File

@@ -0,0 +1,405 @@
# Generated by Django 5.2.5 on 2025-09-15 17:35
import apps.core.choices.fields
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rides", "0020_add_hybrid_filtering_indexes"),
]
operations = [
migrations.AlterField(
model_name="company",
name="roles",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("MANUFACTURER", "Ride Manufacturer"),
("DESIGNER", "Ride Designer"),
],
max_length=20,
),
blank=True,
default=list,
size=None,
),
),
migrations.AlterField(
model_name="companyevent",
name="roles",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("MANUFACTURER", "Ride Manufacturer"),
("DESIGNER", "Ride Designer"),
],
max_length=20,
),
blank=True,
default=list,
size=None,
),
),
migrations.AlterField(
model_name="ride",
name="category",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="categories",
choices=[
("RC", "Roller Coaster"),
("DR", "Dark Ride"),
("FR", "Flat Ride"),
("WR", "Water Ride"),
("TR", "Transport Ride"),
("OT", "Other"),
],
default="",
domain="rides",
help_text="Ride category classification",
max_length=2,
),
),
migrations.AlterField(
model_name="ride",
name="post_closing_status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="post_closing_statuses",
choices=[
("SBNO", "Standing But Not Operating"),
("CLOSED_PERM", "Permanently Closed"),
],
domain="rides",
help_text="Status to change to after closing date",
max_length=20,
null=True,
),
),
migrations.AlterField(
model_name="ride",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="statuses",
choices=[
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("SBNO", "Standing But Not Operating"),
("CLOSING", "Closing"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
],
default="OPERATING",
domain="rides",
help_text="Current operational status of the ride",
max_length=20,
),
),
migrations.AlterField(
model_name="rideevent",
name="category",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="categories",
choices=[
("RC", "Roller Coaster"),
("DR", "Dark Ride"),
("FR", "Flat Ride"),
("WR", "Water Ride"),
("TR", "Transport Ride"),
("OT", "Other"),
],
default="",
domain="rides",
help_text="Ride category classification",
max_length=2,
),
),
migrations.AlterField(
model_name="rideevent",
name="post_closing_status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="post_closing_statuses",
choices=[
("SBNO", "Standing But Not Operating"),
("CLOSED_PERM", "Permanently Closed"),
],
domain="rides",
help_text="Status to change to after closing date",
max_length=20,
null=True,
),
),
migrations.AlterField(
model_name="rideevent",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="statuses",
choices=[
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("SBNO", "Standing But Not Operating"),
("CLOSING", "Closing"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
],
default="OPERATING",
domain="rides",
help_text="Current operational status of the ride",
max_length=20,
),
),
migrations.AlterField(
model_name="ridemodel",
name="category",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="categories",
choices=[
("RC", "Roller Coaster"),
("DR", "Dark Ride"),
("FR", "Flat Ride"),
("WR", "Water Ride"),
("TR", "Transport Ride"),
("OT", "Other"),
],
default="",
domain="rides",
help_text="Primary category classification",
max_length=2,
),
),
migrations.AlterField(
model_name="ridemodel",
name="target_market",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="target_markets",
choices=[
("FAMILY", "Family"),
("THRILL", "Thrill"),
("EXTREME", "Extreme"),
("KIDDIE", "Kiddie"),
("ALL_AGES", "All Ages"),
],
domain="rides",
help_text="Primary target market for this ride model",
max_length=50,
),
),
migrations.AlterField(
model_name="ridemodelevent",
name="category",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="categories",
choices=[
("RC", "Roller Coaster"),
("DR", "Dark Ride"),
("FR", "Flat Ride"),
("WR", "Water Ride"),
("TR", "Transport Ride"),
("OT", "Other"),
],
default="",
domain="rides",
help_text="Primary category classification",
max_length=2,
),
),
migrations.AlterField(
model_name="ridemodelevent",
name="target_market",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="target_markets",
choices=[
("FAMILY", "Family"),
("THRILL", "Thrill"),
("EXTREME", "Extreme"),
("KIDDIE", "Kiddie"),
("ALL_AGES", "All Ages"),
],
domain="rides",
help_text="Primary target market for this ride model",
max_length=50,
),
),
migrations.AlterField(
model_name="ridephoto",
name="photo_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="photo_types",
choices=[
("exterior", "Exterior View"),
("queue", "Queue Area"),
("station", "Station"),
("onride", "On-Ride"),
("construction", "Construction"),
("other", "Other"),
],
default="exterior",
domain="rides",
help_text="Type of photo for categorization and display purposes",
max_length=50,
),
),
migrations.AlterField(
model_name="ridephotoevent",
name="photo_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="photo_types",
choices=[
("exterior", "Exterior View"),
("queue", "Queue Area"),
("station", "Station"),
("onride", "On-Ride"),
("construction", "Construction"),
("other", "Other"),
],
default="exterior",
domain="rides",
help_text="Type of photo for categorization and display purposes",
max_length=50,
),
),
migrations.AlterField(
model_name="rollercoasterstats",
name="launch_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="launch_systems",
choices=[
("CHAIN", "Chain Lift"),
("LSM", "LSM Launch"),
("HYDRAULIC", "Hydraulic Launch"),
("GRAVITY", "Gravity"),
("OTHER", "Other"),
],
default="CHAIN",
domain="rides",
help_text="Launch or lift system type",
max_length=20,
),
),
migrations.AlterField(
model_name="rollercoasterstats",
name="roller_coaster_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="coaster_types",
choices=[
("SITDOWN", "Sit Down"),
("INVERTED", "Inverted"),
("FLYING", "Flying"),
("STANDUP", "Stand Up"),
("WING", "Wing"),
("DIVE", "Dive"),
("FAMILY", "Family"),
("WILD_MOUSE", "Wild Mouse"),
("SPINNING", "Spinning"),
("FOURTH_DIMENSION", "4th Dimension"),
("OTHER", "Other"),
],
default="SITDOWN",
domain="rides",
help_text="Roller coaster type classification",
max_length=20,
),
),
migrations.AlterField(
model_name="rollercoasterstats",
name="track_material",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="track_materials",
choices=[("STEEL", "Steel"), ("WOOD", "Wood"), ("HYBRID", "Hybrid")],
default="STEEL",
domain="rides",
help_text="Track construction material type",
max_length=20,
),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="launch_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="launch_systems",
choices=[
("CHAIN", "Chain Lift"),
("LSM", "LSM Launch"),
("HYDRAULIC", "Hydraulic Launch"),
("GRAVITY", "Gravity"),
("OTHER", "Other"),
],
default="CHAIN",
domain="rides",
help_text="Launch or lift system type",
max_length=20,
),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="roller_coaster_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="coaster_types",
choices=[
("SITDOWN", "Sit Down"),
("INVERTED", "Inverted"),
("FLYING", "Flying"),
("STANDUP", "Stand Up"),
("WING", "Wing"),
("DIVE", "Dive"),
("FAMILY", "Family"),
("WILD_MOUSE", "Wild Mouse"),
("SPINNING", "Spinning"),
("FOURTH_DIMENSION", "4th Dimension"),
("OTHER", "Other"),
],
default="SITDOWN",
domain="rides",
help_text="Roller coaster type classification",
max_length=20,
),
),
migrations.AlterField(
model_name="rollercoasterstatsevent",
name="track_material",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
blank=True,
choice_group="track_materials",
choices=[("STEEL", "Steel"), ("WOOD", "Wood"), ("HYBRID", "Hybrid")],
default="STEEL",
domain="rides",
help_text="Track construction material type",
max_length=20,
),
),
]

View File

@@ -0,0 +1,53 @@
# Generated by Django 5.2.5 on 2025-09-15 18:07
import apps.core.choices.fields
import django.contrib.postgres.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("rides", "0021_alter_company_roles_alter_companyevent_roles_and_more"),
]
operations = [
migrations.AlterField(
model_name="company",
name="roles",
field=django.contrib.postgres.fields.ArrayField(
base_field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="company_roles",
choices=[
("MANUFACTURER", "Ride Manufacturer"),
("DESIGNER", "Ride Designer"),
],
domain="rides",
max_length=20,
),
blank=True,
default=list,
size=None,
),
),
migrations.AlterField(
model_name="companyevent",
name="roles",
field=django.contrib.postgres.fields.ArrayField(
base_field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="company_roles",
choices=[
("MANUFACTURER", "Ride Manufacturer"),
("DESIGNER", "Ride Designer"),
],
domain="rides",
max_length=20,
),
blank=True,
default=list,
size=None,
),
),
]

View File

@@ -0,0 +1,94 @@
# Generated by Django 5.2.5 on 2025-09-15 19:06
import apps.core.choices.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("rides", "0022_alter_company_roles_alter_companyevent_roles"),
]
operations = [
migrations.AlterField(
model_name="ridemodelphoto",
name="photo_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="photo_types",
choices=[
("PROMOTIONAL", "Promotional"),
("TECHNICAL", "Technical Drawing"),
("INSTALLATION", "Installation Example"),
("RENDERING", "3D Rendering"),
("CATALOG", "Catalog Image"),
],
default="PROMOTIONAL",
domain="rides",
help_text="Type of photo for categorization and display purposes",
max_length=20,
),
),
migrations.AlterField(
model_name="ridemodelphotoevent",
name="photo_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="photo_types",
choices=[
("PROMOTIONAL", "Promotional"),
("TECHNICAL", "Technical Drawing"),
("INSTALLATION", "Installation Example"),
("RENDERING", "3D Rendering"),
("CATALOG", "Catalog Image"),
],
default="PROMOTIONAL",
domain="rides",
help_text="Type of photo for categorization and display purposes",
max_length=20,
),
),
migrations.AlterField(
model_name="ridemodeltechnicalspec",
name="spec_category",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="spec_categories",
choices=[
("DIMENSIONS", "Dimensions"),
("PERFORMANCE", "Performance"),
("CAPACITY", "Capacity"),
("SAFETY", "Safety Features"),
("ELECTRICAL", "Electrical Requirements"),
("FOUNDATION", "Foundation Requirements"),
("MAINTENANCE", "Maintenance"),
("OTHER", "Other"),
],
domain="rides",
help_text="Category of technical specification",
max_length=50,
),
),
migrations.AlterField(
model_name="ridemodeltechnicalspecevent",
name="spec_category",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="spec_categories",
choices=[
("DIMENSIONS", "Dimensions"),
("PERFORMANCE", "Performance"),
("CAPACITY", "Capacity"),
("SAFETY", "Safety Features"),
("ELECTRICAL", "Electrical Requirements"),
("FOUNDATION", "Foundation Requirements"),
("MAINTENANCE", "Maintenance"),
("OTHER", "Other"),
],
domain="rides",
help_text="Category of technical specification",
max_length=50,
),
),
]

View File

@@ -8,7 +8,7 @@ The Company model is aliased as Manufacturer to clarify its role as ride manufac
while maintaining backward compatibility through the Company alias.
"""
from .rides import Ride, RideModel, RollerCoasterStats, Categories, CATEGORY_CHOICES
from .rides import Ride, RideModel, RollerCoasterStats
from .company import Company
from .location import RideLocation
from .reviews import RideReview
@@ -28,7 +28,4 @@ __all__ = [
"RideRanking",
"RidePairComparison",
"RankingSnapshot",
# Shared constants
"Categories",
"CATEGORY_CHOICES",
]

View File

@@ -7,20 +7,15 @@ from django.conf import settings
from apps.core.history import HistoricalSlug
from apps.core.models import TrackedModel
from apps.core.choices.fields import RichChoiceField
@pghistory.track()
class Company(TrackedModel):
class CompanyRole(models.TextChoices):
MANUFACTURER = "MANUFACTURER", "Ride Manufacturer"
DESIGNER = "DESIGNER", "Ride Designer"
OPERATOR = "OPERATOR", "Park Operator"
PROPERTY_OWNER = "PROPERTY_OWNER", "Property Owner"
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
roles = ArrayField(
models.CharField(max_length=20, choices=CompanyRole.choices),
RichChoiceField(choice_group="company_roles", domain="rides", max_length=20),
default=list,
blank=True,
)
@@ -64,7 +59,6 @@ class Company(TrackedModel):
def get_absolute_url(self):
# This will need to be updated to handle different roles
return reverse("companies:detail", kwargs={"slug": self.slug})
return "#"
@classmethod
def get_by_slug(cls, slug):
@@ -73,14 +67,19 @@ class Company(TrackedModel):
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Check pghistory first
history_model = cls.get_history_model()
history_entry = (
history_model.objects.filter(slug=slug)
.order_by("-pgh_created_at")
.first()
)
if history_entry:
return cls.objects.get(id=history_entry.pgh_obj_id), True
try:
from django.apps import apps
history_model = apps.get_model('rides', f'{cls.__name__}Event')
history_entry = (
history_model.objects.filter(slug=slug)
.order_by("-pgh_created_at")
.first()
)
if history_entry:
return cls.objects.get(id=history_entry.pgh_obj_id), True
except LookupError:
# History model doesn't exist, skip pghistory check
pass
# Check manual slug history as fallback
try:
@@ -91,7 +90,7 @@ class Company(TrackedModel):
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
raise cls.DoesNotExist("No company found with this slug")
class Meta:
class Meta(TrackedModel.Meta):
app_label = "rides"
ordering = ["name"]
verbose_name_plural = "Companies"

View File

@@ -8,6 +8,7 @@ from typing import Any, Optional, List, cast
from django.db import models
from django.conf import settings
from apps.core.history import TrackedModel
from apps.core.choices import RichChoiceField
from apps.core.services.media_service import MediaService
import pghistory
@@ -48,17 +49,12 @@ class RidePhoto(TrackedModel):
is_approved = models.BooleanField(default=False)
# Ride-specific metadata
photo_type = models.CharField(
photo_type = RichChoiceField(
choice_group="photo_types",
domain="rides",
max_length=50,
choices=[
("exterior", "Exterior View"),
("queue", "Queue Area"),
("station", "Station"),
("onride", "On-Ride"),
("construction", "Construction"),
("other", "Other"),
],
default="exterior",
help_text="Type of photo for categorization and display purposes"
)
# Metadata

View File

@@ -40,7 +40,7 @@ class RideReview(TrackedModel):
)
moderated_at = models.DateTimeField(null=True, blank=True)
class Meta:
class Meta(TrackedModel.Meta):
ordering = ["-created_at"]
unique_together = ["ride", "user"]
constraints = [

View File

@@ -2,22 +2,14 @@ from django.db import models
from django.utils.text import slugify
from config.django import base as settings
from apps.core.models import TrackedModel
from apps.core.choices import RichChoiceField
from .company import Company
import pghistory
from typing import TYPE_CHECKING
# Shared choices that will be used by multiple models
CATEGORY_CHOICES = [
("", "Select ride type"),
("RC", "Roller Coaster"),
("DR", "Dark Ride"),
("FR", "Flat Ride"),
("WR", "Water Ride"),
("TR", "Transport"),
("OT", "Other"),
]
if TYPE_CHECKING:
from .rides import RollerCoasterStats
# Legacy alias for backward compatibility
Categories = CATEGORY_CHOICES
@pghistory.track()
@@ -46,9 +38,10 @@ class RideModel(TrackedModel):
description = models.TextField(
blank=True, help_text="Detailed description of the ride model"
)
category = models.CharField(
category = RichChoiceField(
choice_group="categories",
domain="rides",
max_length=2,
choices=CATEGORY_CHOICES,
default="",
blank=True,
help_text="Primary category classification",
@@ -137,16 +130,11 @@ class RideModel(TrackedModel):
blank=True,
help_text="Notable design features or innovations (JSON or comma-separated)",
)
target_market = models.CharField(
target_market = RichChoiceField(
choice_group="target_markets",
domain="rides",
max_length=50,
blank=True,
choices=[
("FAMILY", "Family"),
("THRILL", "Thrill"),
("EXTREME", "Extreme"),
("KIDDIE", "Kiddie"),
("ALL_AGES", "All Ages"),
],
help_text="Primary target market for this ride model",
)
@@ -371,16 +359,12 @@ class RideModelPhoto(TrackedModel):
alt_text = models.CharField(max_length=255, blank=True)
# Photo metadata
photo_type = models.CharField(
photo_type = RichChoiceField(
choice_group="photo_types",
domain="rides",
max_length=20,
choices=[
("PROMOTIONAL", "Promotional"),
("TECHNICAL", "Technical Drawing"),
("INSTALLATION", "Installation Example"),
("RENDERING", "3D Rendering"),
("CATALOG", "Catalog Image"),
],
default="PROMOTIONAL",
help_text="Type of photo for categorization and display purposes",
)
is_primary = models.BooleanField(
@@ -418,18 +402,11 @@ class RideModelTechnicalSpec(TrackedModel):
RideModel, on_delete=models.CASCADE, related_name="technical_specs"
)
spec_category = models.CharField(
spec_category = RichChoiceField(
choice_group="spec_categories",
domain="rides",
max_length=50,
choices=[
("DIMENSIONS", "Dimensions"),
("PERFORMANCE", "Performance"),
("CAPACITY", "Capacity"),
("SAFETY", "Safety Features"),
("ELECTRICAL", "Electrical Requirements"),
("FOUNDATION", "Foundation Requirements"),
("MAINTENANCE", "Maintenance"),
("OTHER", "Other"),
],
help_text="Category of technical specification",
)
spec_name = models.CharField(max_length=100, help_text="Name of the specification")
@@ -459,23 +436,9 @@ class Ride(TrackedModel):
Note: The average_rating field is denormalized and refreshed by background
jobs. Use selectors or annotations for real-time calculations if needed.
"""
STATUS_CHOICES = [
("", "Select status"),
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("SBNO", "Standing But Not Operating"),
("CLOSING", "Closing"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
]
POST_CLOSING_STATUS_CHOICES = [
("SBNO", "Standing But Not Operating"),
("CLOSED_PERM", "Permanently Closed"),
]
if TYPE_CHECKING:
coaster_stats: 'RollerCoasterStats'
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255)
@@ -490,8 +453,13 @@ class Ride(TrackedModel):
null=True,
blank=True,
)
category = models.CharField(
max_length=2, choices=CATEGORY_CHOICES, default="", blank=True
category = RichChoiceField(
choice_group="categories",
domain="rides",
max_length=2,
default="",
blank=True,
help_text="Ride category classification"
)
manufacturer = models.ForeignKey(
Company,
@@ -517,12 +485,17 @@ class Ride(TrackedModel):
blank=True,
help_text="The specific model/type of this ride",
)
status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default="OPERATING"
)
post_closing_status = models.CharField(
status = RichChoiceField(
choice_group="statuses",
domain="rides",
max_length=20,
default="OPERATING",
help_text="Current operational status of the ride"
)
post_closing_status = RichChoiceField(
choice_group="post_closing_statuses",
domain="rides",
max_length=20,
choices=POST_CLOSING_STATUS_CHOICES,
null=True,
blank=True,
help_text="Status to change to after closing date",
@@ -654,6 +627,14 @@ class Ride(TrackedModel):
# Clear park_area if it doesn't belong to the new park
self.park_area = None
# Sync manufacturer with ride model's manufacturer
if self.ride_model and self.ride_model.manufacturer:
self.manufacturer = self.ride_model.manufacturer
elif self.ride_model and not self.ride_model.manufacturer:
# If ride model has no manufacturer, clear the ride's manufacturer
# to maintain consistency
self.manufacturer = None
# Generate frontend URLs
if self.park:
frontend_domain = getattr(
@@ -701,15 +682,15 @@ class Ride(TrackedModel):
# Category
if self.category:
category_display = dict(CATEGORY_CHOICES).get(self.category, '')
if category_display:
search_parts.append(category_display)
category_choice = self.get_category_rich_choice()
if category_choice:
search_parts.append(category_choice.label)
# Status
if self.status:
status_display = dict(self.STATUS_CHOICES).get(self.status, '')
if status_display:
search_parts.append(status_display)
status_choice = self.get_status_rich_choice()
if status_choice:
search_parts.append(status_choice.label)
# Companies
if self.manufacturer:
@@ -725,22 +706,22 @@ class Ride(TrackedModel):
# Roller coaster stats if available
try:
if hasattr(self, 'coaster_stats') and self.coaster_stats:
stats = self.coaster_stats
if hasattr(self, 'coaster_stats') and self.coaster_stats:
stats = self.coaster_stats
if stats.track_type:
search_parts.append(stats.track_type)
if stats.track_material:
material_display = dict(RollerCoasterStats.TRACK_MATERIAL_CHOICES).get(stats.track_material, '')
if material_display:
search_parts.append(material_display)
material_choice = stats.get_track_material_rich_choice()
if material_choice:
search_parts.append(material_choice.label)
if stats.roller_coaster_type:
type_display = dict(RollerCoasterStats.COASTER_TYPE_CHOICES).get(stats.roller_coaster_type, '')
if type_display:
search_parts.append(type_display)
type_choice = stats.get_roller_coaster_type_rich_choice()
if type_choice:
search_parts.append(type_choice.label)
if stats.launch_type:
launch_display = dict(RollerCoasterStats.LAUNCH_CHOICES).get(stats.launch_type, '')
if launch_display:
search_parts.append(launch_display)
launch_choice = stats.get_launch_type_rich_choice()
if launch_choice:
search_parts.append(launch_choice.label)
if stats.train_style:
search_parts.append(stats.train_style)
except Exception:
@@ -815,38 +796,53 @@ class Ride(TrackedModel):
return changes
@classmethod
def get_by_slug(cls, slug: str, park=None) -> tuple["Ride", bool]:
"""Get ride by current or historical slug, optionally within a specific park"""
from django.contrib.contenttypes.models import ContentType
from apps.core.history import HistoricalSlug
# Build base query
base_query = cls.objects
if park:
base_query = base_query.filter(park=park)
try:
ride = base_query.get(slug=slug)
return ride, False
except cls.DoesNotExist:
# Try historical slugs in HistoricalSlug model
content_type = ContentType.objects.get_for_model(cls)
historical_query = HistoricalSlug.objects.filter(
content_type=content_type, slug=slug
).order_by("-created_at")
for historical in historical_query:
try:
ride = base_query.get(pk=historical.object_id)
return ride, True
except cls.DoesNotExist:
continue
# Try pghistory events
event_model = getattr(cls, "event_model", None)
if event_model:
historical_events = event_model.objects.filter(slug=slug).order_by("-pgh_created_at")
for historical_event in historical_events:
try:
ride = base_query.get(pk=historical_event.pgh_obj_id)
return ride, True
except cls.DoesNotExist:
continue
raise cls.DoesNotExist("No ride found with this slug")
@pghistory.track()
class RollerCoasterStats(models.Model):
"""Model for tracking roller coaster specific statistics"""
TRACK_MATERIAL_CHOICES = [
("STEEL", "Steel"),
("WOOD", "Wood"),
("HYBRID", "Hybrid"),
]
COASTER_TYPE_CHOICES = [
("SITDOWN", "Sit Down"),
("INVERTED", "Inverted"),
("FLYING", "Flying"),
("STANDUP", "Stand Up"),
("WING", "Wing"),
("DIVE", "Dive"),
("FAMILY", "Family"),
("WILD_MOUSE", "Wild Mouse"),
("SPINNING", "Spinning"),
("FOURTH_DIMENSION", "4th Dimension"),
("OTHER", "Other"),
]
LAUNCH_CHOICES = [
("CHAIN", "Chain Lift"),
("LSM", "LSM Launch"),
("HYDRAULIC", "Hydraulic Launch"),
("GRAVITY", "Gravity"),
("OTHER", "Other"),
]
ride = models.OneToOneField(
Ride, on_delete=models.CASCADE, related_name="coaster_stats"
@@ -863,23 +859,31 @@ class RollerCoasterStats(models.Model):
inversions = models.PositiveIntegerField(default=0)
ride_time_seconds = models.PositiveIntegerField(null=True, blank=True)
track_type = models.CharField(max_length=255, blank=True)
track_material = models.CharField(
track_material = RichChoiceField(
choice_group="track_materials",
domain="rides",
max_length=20,
choices=TRACK_MATERIAL_CHOICES,
default="STEEL",
blank=True,
help_text="Track construction material type"
)
roller_coaster_type = models.CharField(
roller_coaster_type = RichChoiceField(
choice_group="coaster_types",
domain="rides",
max_length=20,
choices=COASTER_TYPE_CHOICES,
default="SITDOWN",
blank=True,
help_text="Roller coaster type classification"
)
max_drop_height_ft = models.DecimalField(
max_digits=6, decimal_places=2, null=True, blank=True
)
launch_type = models.CharField(
max_length=20, choices=LAUNCH_CHOICES, default="CHAIN"
launch_type = RichChoiceField(
choice_group="launch_systems",
domain="rides",
max_length=20,
default="CHAIN",
help_text="Launch or lift system type"
)
train_style = models.CharField(max_length=255, blank=True)
trains_count = models.PositiveIntegerField(null=True, blank=True)

View File

@@ -8,7 +8,8 @@ from django.db.models import QuerySet, Q, Count, Avg, Prefetch
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance
from .models import Ride, RideModel, RideReview, CATEGORY_CHOICES
from .models import Ride, RideModel, RideReview
from .choices import RIDE_CATEGORIES
def ride_list_for_display(
@@ -275,10 +276,10 @@ def ride_statistics_by_category() -> Dict[str, Any]:
"""
stats = {}
for category_code, category_name in CATEGORY_CHOICES:
if category_code: # Skip empty choice
count = Ride.objects.filter(category=category_code).count()
stats[category_code] = {"name": category_name, "count": count}
for category in RIDE_CATEGORIES:
if category.value: # Skip empty choice
count = Ride.objects.filter(category=category.value).count()
stats[category.value] = {"name": category.label, "count": count}
return stats

View File

@@ -17,12 +17,10 @@ Architecture:
- Hybrid: Combine both approaches based on data characteristics
"""
from typing import Dict, List, Any, Optional, Tuple
from typing import Dict, List, Any, Optional
from django.core.cache import cache
from django.db import models
from django.db.models import Q, Count, Min, Max, Avg
from django.utils import timezone
from datetime import timedelta
from django.db.models import Q, Min, Max
import logging
logger = logging.getLogger(__name__)
@@ -67,7 +65,6 @@ class SmartRideLoader:
- has_more: Whether more data is available
- filter_metadata: Available filter options
"""
from apps.rides.models import Ride
# Get total count for strategy decision
total_count = self._get_total_count(filters)
@@ -89,7 +86,6 @@ class SmartRideLoader:
Returns:
Dict containing additional ride records
"""
from apps.rides.models import Ride
# Build queryset with filters
queryset = self._build_filtered_queryset(filters)
@@ -713,7 +709,10 @@ class SmartRideLoader:
'TR': 'Transport Ride',
'OT': 'Other',
}
return category_labels.get(category, category)
if category in category_labels:
return category_labels[category]
else:
raise ValueError(f"Unknown ride category: {category}")
def _get_status_label(self, status: str) -> str:
"""Convert status code to human-readable label."""
@@ -727,7 +726,10 @@ class SmartRideLoader:
'DEMOLISHED': 'Demolished',
'RELOCATED': 'Relocated',
}
return status_labels.get(status, status)
if status in status_labels:
return status_labels[status]
else:
raise ValueError(f"Unknown ride status: {status}")
def _get_rc_type_label(self, rc_type: str) -> str:
"""Convert roller coaster type to human-readable label."""
@@ -745,7 +747,10 @@ class SmartRideLoader:
'PIPELINE': 'Pipeline',
'FOURTH_DIMENSION': '4th Dimension',
}
return rc_type_labels.get(rc_type, rc_type.replace('_', ' ').title())
if rc_type in rc_type_labels:
return rc_type_labels[rc_type]
else:
raise ValueError(f"Unknown roller coaster type: {rc_type}")
def _get_track_material_label(self, material: str) -> str:
"""Convert track material to human-readable label."""
@@ -754,7 +759,10 @@ class SmartRideLoader:
'WOOD': 'Wood',
'HYBRID': 'Hybrid (Steel/Wood)',
}
return material_labels.get(material, material)
if material in material_labels:
return material_labels[material]
else:
raise ValueError(f"Unknown track material: {material}")
def _get_launch_type_label(self, launch_type: str) -> str:
"""Convert launch type to human-readable label."""
@@ -768,4 +776,7 @@ class SmartRideLoader:
'FLYWHEEL': 'Flywheel Launch',
'NONE': 'No Launch System',
}
return launch_labels.get(launch_type, launch_type.replace('_', ' ').title())
if launch_type in launch_labels:
return launch_labels[launch_type]
else:
raise ValueError(f"Unknown launch type: {launch_type}")

View File

@@ -595,7 +595,10 @@ class RideSearchService:
"founded_date_range": "Founded Date",
}
return display_names.get(filter_key, filter_key.replace("_", " ").title())
if filter_key in display_names:
return display_names[filter_key]
else:
raise ValueError(f"Unknown filter key: {filter_key}")
def get_search_suggestions(
self, query: str, limit: int = 10

View File

@@ -1,6 +1,6 @@
from django import template
from django.templatetags.static import static
from ..models.rides import Categories
from ..choices import RIDE_CATEGORIES
register = template.Library()
@@ -16,7 +16,10 @@ def get_ride_placeholder_image(category):
"TR": "images/placeholders/transport.jpg",
"OT": "images/placeholders/other-ride.jpg",
}
return static(category_images.get(category, "images/placeholders/default-ride.jpg"))
if category in category_images:
return static(category_images[category])
else:
raise ValueError(f"Unknown ride category: {category}")
@register.simple_tag
@@ -28,4 +31,8 @@ def get_park_placeholder_image():
@register.filter
def get_category_display(code):
"""Convert category code to display name"""
return dict(Categories).get(code, code)
choices = {choice.value: choice.label for choice in RIDE_CATEGORIES}
if code in choices:
return choices[code]
else:
raise ValueError(f"Unknown ride category code: {code}")

View File

@@ -8,25 +8,25 @@ urlpatterns = [
path("", views.RideListView.as_view(), name="global_ride_list"),
# Global category views
path(
"roller_coasters/",
"roller-coasters/",
views.SingleCategoryListView.as_view(),
{"category": "RC"},
name="global_roller_coasters",
),
path(
"dark_rides/",
"dark-rides/",
views.SingleCategoryListView.as_view(),
{"category": "DR"},
name="global_dark_rides",
),
path(
"flat_rides/",
"flat-rides/",
views.SingleCategoryListView.as_view(),
{"category": "FR"},
name="global_flat_rides",
),
path(
"water_rides/",
"water-rides/",
views.SingleCategoryListView.as_view(),
{"category": "WR"},
name="global_water_rides",

View File

@@ -5,7 +5,8 @@ from django.db.models import Q
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse, Http404
from django.db.models import Count
from .models.rides import Ride, RideModel, Categories
from .models.rides import Ride, RideModel
from .choices import RIDE_CATEGORIES
from .models.company import Company
from .forms import RideForm, RideSearchForm
from .forms.search import MasterFilterForm
@@ -276,7 +277,10 @@ class RideListView(ListView):
# Add filter form
filter_form = MasterFilterForm(self.request.GET)
context["filter_form"] = filter_form
context["category_choices"] = Categories
# Use Rich Choice registry directly
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
context["category_choices"] = [(choice.value, choice.label) for choice in choices]
# Add filter summary for display
if filter_form.is_valid():
@@ -324,7 +328,11 @@ class SingleCategoryListView(ListView):
if hasattr(self, "park"):
context["park"] = self.park
context["park_slug"] = self.kwargs["park_slug"]
context["category"] = dict(Categories).get(self.kwargs["category"])
# Find the category choice by value using Rich Choice registry
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
category_choice = next((choice for choice in choices if choice.value == self.kwargs["category"]), None)
context["category"] = category_choice.label if category_choice else "Unknown"
return context
@@ -419,14 +427,16 @@ def get_search_suggestions(request: HttpRequest) -> HttpResponse:
)
# Add category matches
for code, name in Categories:
if query in name.lower():
ride_count = Ride.objects.filter(category=code).count()
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
for choice in choices:
if query in choice.label.lower():
ride_count = Ride.objects.filter(category=choice.value).count()
suggestions.append(
{
"type": "category",
"code": code,
"text": name,
"code": choice.value,
"text": choice.label,
"count": ride_count,
}
)
@@ -517,7 +527,10 @@ class RideRankingsView(ListView):
def get_context_data(self, **kwargs):
"""Add context for rankings view."""
context = super().get_context_data(**kwargs)
context["category_choices"] = Categories
# Use Rich Choice registry directly
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
context["category_choices"] = [(choice.value, choice.label) for choice in choices]
context["selected_category"] = self.request.GET.get("category", "all")
context["min_riders"] = self.request.GET.get("min_riders", "")