This commit is contained in:
pacnpal
2026-01-02 07:58:58 -05:00
parent b243b17af7
commit 1adba1b804
36 changed files with 6345 additions and 6 deletions

View File

@@ -12,19 +12,23 @@ from .company import Company
from .credits import RideCredit
from .location import RideLocation
from .media import RidePhoto
from .name_history import RideNameHistory
from .rankings import RankingSnapshot, RidePairComparison, RideRanking
from .reviews import RideReview
from .rides import Ride, RideModel, RollerCoasterStats
from .stats import DarkRideStats, FlatRideStats, WaterRideStats
from .stats import DarkRideStats, FlatRideStats, KiddieRideStats, TransportationStats, WaterRideStats
__all__ = [
# Primary models
"Ride",
"RideModel",
"RideNameHistory",
"RollerCoasterStats",
"WaterRideStats",
"DarkRideStats",
"FlatRideStats",
"KiddieRideStats",
"TransportationStats",
"Company",
"RideLocation",
"RideReview",
@@ -35,3 +39,4 @@ __all__ = [
"RidePairComparison",
"RankingSnapshot",
]

View File

@@ -0,0 +1,73 @@
"""
Ride Name History model for tracking historical ride names.
This model stores the history of name changes for rides, enabling display of
former names on ride detail pages.
"""
import pghistory
from django.db import models
from apps.core.models import TrackedModel
@pghistory.track()
class RideNameHistory(TrackedModel):
"""
Tracks historical names of rides.
When a ride is renamed, this model stores the previous name along with
the year range it was used and an optional reason for the change.
"""
ride = models.ForeignKey(
"rides.Ride",
on_delete=models.CASCADE,
related_name="former_names",
help_text="The ride this name history entry belongs to",
)
former_name = models.CharField(
max_length=200,
help_text="The previous name of the ride",
)
from_year = models.PositiveSmallIntegerField(
null=True,
blank=True,
help_text="Year the ride started using this name",
)
to_year = models.PositiveSmallIntegerField(
null=True,
blank=True,
help_text="Year the ride stopped using this name",
)
reason = models.CharField(
max_length=500,
blank=True,
help_text="Reason for the name change (e.g., 'Retheme to Peanuts')",
)
class Meta(TrackedModel.Meta):
verbose_name = "Ride Name History"
verbose_name_plural = "Ride Name Histories"
ordering = ["-to_year", "-from_year"]
indexes = [
models.Index(fields=["ride", "-to_year"]),
]
def __str__(self):
year_range = ""
if self.from_year and self.to_year:
year_range = f" ({self.from_year}-{self.to_year})"
elif self.to_year:
year_range = f" (until {self.to_year})"
elif self.from_year:
year_range = f" (from {self.from_year})"
return f"{self.former_name}{year_range}"
def clean(self):
from django.core.exceptions import ValidationError
if self.from_year and self.to_year and self.from_year > self.to_year:
raise ValidationError(
{"from_year": "From year cannot be after to year."}
)

View File

@@ -508,7 +508,21 @@ class Ride(StateMachineMixin, TrackedModel):
help_text="Status to change to after closing date",
)
opening_date = models.DateField(null=True, blank=True)
opening_date_precision = models.CharField(
max_length=10,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
blank=True,
help_text="Precision of the opening date",
)
closing_date = models.DateField(null=True, blank=True)
closing_date_precision = models.CharField(
max_length=10,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
blank=True,
help_text="Precision of the closing date",
)
status_since = models.DateField(null=True, blank=True)
min_height_in = models.PositiveIntegerField(null=True, blank=True)
max_height_in = models.PositiveIntegerField(null=True, blank=True)
@@ -516,6 +530,18 @@ class Ride(StateMachineMixin, TrackedModel):
ride_duration_seconds = models.PositiveIntegerField(null=True, blank=True)
average_rating = models.DecimalField(max_digits=3, decimal_places=2, null=True, blank=True)
# Additional ride classification
ride_sub_type = models.CharField(
max_length=100,
blank=True,
help_text="Sub-category of ride (e.g., 'Flying Coaster', 'Inverted Coaster', 'Log Flume')",
)
age_requirement = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Minimum age requirement in years (if any)",
)
# Computed fields for hybrid filtering
opening_year = models.IntegerField(null=True, blank=True, db_index=True)
search_text = models.TextField(blank=True, db_index=True)
@@ -680,6 +706,14 @@ class Ride(StateMachineMixin, TrackedModel):
self.save()
@property
def is_closing(self) -> bool:
"""Returns True if this ride has a closing date in the future (announced closure)."""
from django.utils import timezone
if self.closing_date:
return self.closing_date > timezone.now().date()
return False
def save(self, *args, **kwargs) -> None:
# Handle slug generation and conflicts
if not self.slug:

View File

@@ -224,3 +224,152 @@ class FlatRideStats(TrackedModel):
def __str__(self) -> str:
return f"Flat Ride Stats for {self.ride.name}"
# Transport Type Choices for Transportation Rides
TRANSPORT_TYPES = [
("TRAIN", "Train"),
("MONORAIL", "Monorail"),
("SKYLIFT", "Skylift / Chairlift"),
("FERRY", "Ferry / Boat"),
("PEOPLEMOVER", "PeopleMover"),
("CABLE_CAR", "Cable Car"),
("TRAM", "Tram"),
]
@pghistory.track()
class KiddieRideStats(TrackedModel):
"""
Statistics specific to kiddie rides (category=KR).
Tracks age-appropriate ride characteristics and theming.
"""
ride = models.OneToOneField(
"rides.Ride",
on_delete=models.CASCADE,
related_name="kiddie_stats",
help_text="Ride these kiddie ride statistics belong to",
)
min_age = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Minimum recommended age in years",
)
max_age = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Maximum recommended age in years",
)
educational_theme = models.CharField(
max_length=200,
blank=True,
help_text="Educational theme if applicable (e.g., 'Dinosaurs', 'Space')",
)
character_theme = models.CharField(
max_length=200,
blank=True,
help_text="Character theme if applicable (e.g., 'Paw Patrol', 'Peppa Pig')",
)
guardian_required = models.BooleanField(
default=False,
help_text="Whether a guardian must be present during the ride",
)
adult_ride_along = models.BooleanField(
default=True,
help_text="Whether adults can ride along with children",
)
seats_per_vehicle = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Number of seats per ride vehicle",
)
class Meta(TrackedModel.Meta):
verbose_name = "Kiddie Ride Statistics"
verbose_name_plural = "Kiddie Ride Statistics"
ordering = ["ride"]
def __str__(self) -> str:
return f"Kiddie Ride Stats for {self.ride.name}"
@pghistory.track()
class TransportationStats(TrackedModel):
"""
Statistics specific to transportation rides (category=TR).
Tracks route, capacity, and vehicle information.
"""
ride = models.OneToOneField(
"rides.Ride",
on_delete=models.CASCADE,
related_name="transport_stats",
help_text="Ride these transportation statistics belong to",
)
transport_type = models.CharField(
max_length=20,
choices=TRANSPORT_TYPES,
default="TRAIN",
help_text="Type of transportation",
)
route_length_ft = models.DecimalField(
max_digits=8,
decimal_places=2,
null=True,
blank=True,
help_text="Total route length in feet",
)
stations_count = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Number of stations/stops on the route",
)
vehicle_capacity = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Passenger capacity per vehicle",
)
vehicles_count = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Total number of vehicles in operation",
)
round_trip_duration_minutes = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Duration of a complete round trip in minutes",
)
scenic_highlights = models.TextField(
blank=True,
help_text="Notable scenic views or attractions along the route",
)
is_one_way = models.BooleanField(
default=False,
help_text="Whether this is a one-way transportation (vs round-trip)",
)
class Meta(TrackedModel.Meta):
verbose_name = "Transportation Statistics"
verbose_name_plural = "Transportation Statistics"
ordering = ["ride"]
def __str__(self) -> str:
return f"Transportation Stats for {self.ride.name}"