mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:31:07 -05:00
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:
@@ -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']
|
||||
|
||||
@@ -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):
|
||||
|
||||
804
backend/apps/rides/choices.py
Normal file
804
backend/apps/rides/choices.py
Normal 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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user