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

@@ -233,12 +233,16 @@ class HybridParkSerializer(serializers.ModelSerializer):
# Company fields
operator_name = serializers.CharField(source="operator.name", read_only=True)
operator_id = serializers.IntegerField(source="operator.id", read_only=True, allow_null=True)
property_owner_name = serializers.CharField(source="property_owner.name", read_only=True, allow_null=True)
# Image URLs for display
banner_image_url = serializers.SerializerMethodField()
card_image_url = serializers.SerializerMethodField()
# Computed property
is_closing = serializers.SerializerMethodField()
# Computed fields for filtering
opening_year = serializers.IntegerField(read_only=True)
search_text = serializers.CharField(read_only=True)
@@ -309,6 +313,11 @@ class HybridParkSerializer(serializers.ModelSerializer):
return obj.card_image.image.url
return None
@extend_schema_field(serializers.BooleanField())
def get_is_closing(self, obj):
"""Check if park has an announced closing date in the future."""
return obj.is_closing
class Meta:
model = Park
fields = [
@@ -321,7 +330,10 @@ class HybridParkSerializer(serializers.ModelSerializer):
"park_type",
# Dates and computed fields
"opening_date",
"opening_date_precision",
"closing_date",
"closing_date_precision",
"is_closing",
"opening_year",
"operating_season",
# Location fields
@@ -333,12 +345,17 @@ class HybridParkSerializer(serializers.ModelSerializer):
"longitude",
# Company relationships
"operator_name",
"operator_id",
"property_owner_name",
# Statistics
"size_acres",
"average_rating",
"ride_count",
"coaster_count",
# Contact info
"phone",
"email",
"timezone",
# Images
"banner_image_url",
"card_image_url",

View File

@@ -491,6 +491,374 @@ class HybridRideSerializer(serializers.ModelSerializer):
return obj.card_image.image.url
return None
# Computed property
is_closing = serializers.SerializerMethodField()
@extend_schema_field(serializers.BooleanField())
def get_is_closing(self, obj):
"""Check if ride has an announced closing date in the future."""
return obj.is_closing
# Water ride stats fields
water_wetness_level = serializers.SerializerMethodField()
water_splash_height_ft = serializers.SerializerMethodField()
water_has_splash_zone = serializers.SerializerMethodField()
water_boat_capacity = serializers.SerializerMethodField()
water_uses_flume = serializers.SerializerMethodField()
water_rapids_sections = serializers.SerializerMethodField()
@extend_schema_field(serializers.CharField(allow_null=True))
def get_water_wetness_level(self, obj):
try:
if hasattr(obj, "water_stats") and obj.water_stats:
return obj.water_stats.wetness_level
return None
except AttributeError:
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_water_splash_height_ft(self, obj):
try:
if hasattr(obj, "water_stats") and obj.water_stats:
return float(obj.water_stats.splash_height_ft) if obj.water_stats.splash_height_ft else None
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_water_has_splash_zone(self, obj):
try:
if hasattr(obj, "water_stats") and obj.water_stats:
return obj.water_stats.has_splash_zone
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_water_boat_capacity(self, obj):
try:
if hasattr(obj, "water_stats") and obj.water_stats:
return obj.water_stats.boat_capacity
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_water_uses_flume(self, obj):
try:
if hasattr(obj, "water_stats") and obj.water_stats:
return obj.water_stats.uses_flume
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_water_rapids_sections(self, obj):
try:
if hasattr(obj, "water_stats") and obj.water_stats:
return obj.water_stats.rapids_sections
return None
except AttributeError:
return None
# Dark ride stats fields
dark_scene_count = serializers.SerializerMethodField()
dark_animatronic_count = serializers.SerializerMethodField()
dark_has_projection_technology = serializers.SerializerMethodField()
dark_is_interactive = serializers.SerializerMethodField()
dark_ride_system = serializers.SerializerMethodField()
dark_uses_practical_effects = serializers.SerializerMethodField()
dark_uses_motion_base = serializers.SerializerMethodField()
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_dark_scene_count(self, obj):
try:
if hasattr(obj, "dark_stats") and obj.dark_stats:
return obj.dark_stats.scene_count
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_dark_animatronic_count(self, obj):
try:
if hasattr(obj, "dark_stats") and obj.dark_stats:
return obj.dark_stats.animatronic_count
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_dark_has_projection_technology(self, obj):
try:
if hasattr(obj, "dark_stats") and obj.dark_stats:
return obj.dark_stats.has_projection_technology
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_dark_is_interactive(self, obj):
try:
if hasattr(obj, "dark_stats") and obj.dark_stats:
return obj.dark_stats.is_interactive
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_dark_ride_system(self, obj):
try:
if hasattr(obj, "dark_stats") and obj.dark_stats:
return obj.dark_stats.ride_system
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_dark_uses_practical_effects(self, obj):
try:
if hasattr(obj, "dark_stats") and obj.dark_stats:
return obj.dark_stats.uses_practical_effects
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_dark_uses_motion_base(self, obj):
try:
if hasattr(obj, "dark_stats") and obj.dark_stats:
return obj.dark_stats.uses_motion_base
return None
except AttributeError:
return None
# Flat ride stats fields
flat_max_height_ft = serializers.SerializerMethodField()
flat_rotation_speed_rpm = serializers.SerializerMethodField()
flat_swing_angle_degrees = serializers.SerializerMethodField()
flat_motion_type = serializers.SerializerMethodField()
flat_arm_count = serializers.SerializerMethodField()
flat_seats_per_gondola = serializers.SerializerMethodField()
flat_max_g_force = serializers.SerializerMethodField()
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_flat_max_height_ft(self, obj):
try:
if hasattr(obj, "flat_stats") and obj.flat_stats:
return float(obj.flat_stats.max_height_ft) if obj.flat_stats.max_height_ft else None
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_flat_rotation_speed_rpm(self, obj):
try:
if hasattr(obj, "flat_stats") and obj.flat_stats:
return float(obj.flat_stats.rotation_speed_rpm) if obj.flat_stats.rotation_speed_rpm else None
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_flat_swing_angle_degrees(self, obj):
try:
if hasattr(obj, "flat_stats") and obj.flat_stats:
return obj.flat_stats.swing_angle_degrees
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_flat_motion_type(self, obj):
try:
if hasattr(obj, "flat_stats") and obj.flat_stats:
return obj.flat_stats.motion_type
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_flat_arm_count(self, obj):
try:
if hasattr(obj, "flat_stats") and obj.flat_stats:
return obj.flat_stats.arm_count
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_flat_seats_per_gondola(self, obj):
try:
if hasattr(obj, "flat_stats") and obj.flat_stats:
return obj.flat_stats.seats_per_gondola
return None
except AttributeError:
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_flat_max_g_force(self, obj):
try:
if hasattr(obj, "flat_stats") and obj.flat_stats:
return float(obj.flat_stats.max_g_force) if obj.flat_stats.max_g_force else None
return None
except (AttributeError, TypeError):
return None
# Kiddie ride stats fields
kiddie_min_age = serializers.SerializerMethodField()
kiddie_max_age = serializers.SerializerMethodField()
kiddie_educational_theme = serializers.SerializerMethodField()
kiddie_character_theme = serializers.SerializerMethodField()
kiddie_guardian_required = serializers.SerializerMethodField()
kiddie_adult_ride_along = serializers.SerializerMethodField()
kiddie_seats_per_vehicle = serializers.SerializerMethodField()
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_kiddie_min_age(self, obj):
try:
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
return obj.kiddie_stats.min_age
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_kiddie_max_age(self, obj):
try:
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
return obj.kiddie_stats.max_age
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_kiddie_educational_theme(self, obj):
try:
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
return obj.kiddie_stats.educational_theme or None
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_kiddie_character_theme(self, obj):
try:
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
return obj.kiddie_stats.character_theme or None
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_kiddie_guardian_required(self, obj):
try:
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
return obj.kiddie_stats.guardian_required
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_kiddie_adult_ride_along(self, obj):
try:
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
return obj.kiddie_stats.adult_ride_along
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_kiddie_seats_per_vehicle(self, obj):
try:
if hasattr(obj, "kiddie_stats") and obj.kiddie_stats:
return obj.kiddie_stats.seats_per_vehicle
return None
except AttributeError:
return None
# Transportation stats fields
transport_type = serializers.SerializerMethodField()
transport_route_length_ft = serializers.SerializerMethodField()
transport_stations_count = serializers.SerializerMethodField()
transport_vehicle_capacity = serializers.SerializerMethodField()
transport_vehicles_count = serializers.SerializerMethodField()
transport_round_trip_duration_minutes = serializers.SerializerMethodField()
transport_scenic_highlights = serializers.SerializerMethodField()
transport_is_one_way = serializers.SerializerMethodField()
@extend_schema_field(serializers.CharField(allow_null=True))
def get_transport_type(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return obj.transport_stats.transport_type
return None
except AttributeError:
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_transport_route_length_ft(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return float(obj.transport_stats.route_length_ft) if obj.transport_stats.route_length_ft else None
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_transport_stations_count(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return obj.transport_stats.stations_count
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_transport_vehicle_capacity(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return obj.transport_stats.vehicle_capacity
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_transport_vehicles_count(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return obj.transport_stats.vehicles_count
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_transport_round_trip_duration_minutes(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return obj.transport_stats.round_trip_duration_minutes
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_transport_scenic_highlights(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return obj.transport_stats.scenic_highlights or None
return None
except AttributeError:
return None
@extend_schema_field(serializers.BooleanField(allow_null=True))
def get_transport_is_one_way(self, obj):
try:
if hasattr(obj, "transport_stats") and obj.transport_stats:
return obj.transport_stats.is_one_way
return None
except AttributeError:
return None
class Meta:
model = Ride
fields = [
@@ -504,7 +872,10 @@ class HybridRideSerializer(serializers.ModelSerializer):
"post_closing_status",
# Dates and computed fields
"opening_date",
"opening_date_precision",
"closing_date",
"closing_date_precision",
"is_closing",
"status_since",
"opening_year",
# Park fields
@@ -533,6 +904,9 @@ class HybridRideSerializer(serializers.ModelSerializer):
"capacity_per_hour",
"ride_duration_seconds",
"average_rating",
# Additional classification
"ride_sub_type",
"age_requirement",
# Roller coaster stats
"coaster_height_ft",
"coaster_length_ft",
@@ -548,6 +922,46 @@ class HybridRideSerializer(serializers.ModelSerializer):
"coaster_trains_count",
"coaster_cars_per_train",
"coaster_seats_per_car",
# Water ride stats
"water_wetness_level",
"water_splash_height_ft",
"water_has_splash_zone",
"water_boat_capacity",
"water_uses_flume",
"water_rapids_sections",
# Dark ride stats
"dark_scene_count",
"dark_animatronic_count",
"dark_has_projection_technology",
"dark_is_interactive",
"dark_ride_system",
"dark_uses_practical_effects",
"dark_uses_motion_base",
# Flat ride stats
"flat_max_height_ft",
"flat_rotation_speed_rpm",
"flat_swing_angle_degrees",
"flat_motion_type",
"flat_arm_count",
"flat_seats_per_gondola",
"flat_max_g_force",
# Kiddie ride stats
"kiddie_min_age",
"kiddie_max_age",
"kiddie_educational_theme",
"kiddie_character_theme",
"kiddie_guardian_required",
"kiddie_adult_ride_along",
"kiddie_seats_per_vehicle",
# Transportation stats
"transport_type",
"transport_route_length_ft",
"transport_stations_count",
"transport_vehicle_capacity",
"transport_vehicles_count",
"transport_round_trip_duration_minutes",
"transport_scenic_highlights",
"transport_is_one_way",
# Images
"banner_image_url",
"card_image_url",

View File

@@ -32,9 +32,18 @@ from .shared import ModelChoices
"roles": ["OPERATOR", "PROPERTY_OWNER"],
"description": "Theme park operator based in Ohio",
"website": "https://cedarfair.com",
"founded_date": "1983-01-01",
"person_type": "CORPORATION",
"status": "ACTIVE",
"founded_year": 1983,
"founded_date": "1983-05-01",
"founded_date_precision": "MONTH",
"logo_url": "https://example.com/logo.png",
"banner_image_url": "https://example.com/banner.jpg",
"card_image_url": "https://example.com/card.jpg",
"average_rating": 4.5,
"review_count": 150,
"parks_count": 11,
"rides_count": 0,
"coasters_count": 0,
},
)
]
@@ -42,15 +51,35 @@ from .shared import ModelChoices
class CompanyDetailOutputSerializer(serializers.Serializer):
"""Output serializer for company details."""
# Core fields
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
roles = serializers.ListField(child=serializers.CharField())
description = serializers.CharField()
website = serializers.URLField()
website = serializers.URLField(required=False, allow_blank=True)
# Entity type and status (ported from legacy)
person_type = serializers.CharField(required=False, allow_blank=True)
status = serializers.CharField()
# Founding information
founded_year = serializers.IntegerField(allow_null=True)
founded_date = serializers.DateField(allow_null=True)
founded_date_precision = serializers.CharField(required=False, allow_blank=True)
# Image URLs
logo_url = serializers.URLField(required=False, allow_blank=True)
banner_image_url = serializers.URLField(required=False, allow_blank=True)
card_image_url = serializers.URLField(required=False, allow_blank=True)
# Rating and review aggregates
average_rating = serializers.DecimalField(max_digits=3, decimal_places=2, allow_null=True)
review_count = serializers.IntegerField()
# Counts
parks_count = serializers.IntegerField()
rides_count = serializers.IntegerField()
coasters_count = serializers.IntegerField()
# Metadata
created_at = serializers.DateTimeField()
@@ -67,7 +96,31 @@ class CompanyCreateInputSerializer(serializers.Serializer):
)
description = serializers.CharField(allow_blank=True, default="")
website = serializers.URLField(required=False, allow_blank=True)
# Entity type and status
person_type = serializers.ChoiceField(
choices=["INDIVIDUAL", "FIRM", "ORGANIZATION", "CORPORATION", "PARTNERSHIP", "GOVERNMENT"],
required=False,
allow_blank=True,
)
status = serializers.ChoiceField(
choices=["ACTIVE", "DEFUNCT", "MERGED", "ACQUIRED", "RENAMED", "DORMANT"],
default="ACTIVE",
)
# Founding information
founded_year = serializers.IntegerField(required=False, allow_null=True)
founded_date = serializers.DateField(required=False, allow_null=True)
founded_date_precision = serializers.ChoiceField(
choices=["YEAR", "MONTH", "DAY"],
required=False,
allow_blank=True,
)
# Image URLs
logo_url = serializers.URLField(required=False, allow_blank=True)
banner_image_url = serializers.URLField(required=False, allow_blank=True)
card_image_url = serializers.URLField(required=False, allow_blank=True)
class CompanyUpdateInputSerializer(serializers.Serializer):
@@ -80,7 +133,31 @@ class CompanyUpdateInputSerializer(serializers.Serializer):
)
description = serializers.CharField(allow_blank=True, required=False)
website = serializers.URLField(required=False, allow_blank=True)
# Entity type and status
person_type = serializers.ChoiceField(
choices=["INDIVIDUAL", "FIRM", "ORGANIZATION", "CORPORATION", "PARTNERSHIP", "GOVERNMENT"],
required=False,
allow_blank=True,
)
status = serializers.ChoiceField(
choices=["ACTIVE", "DEFUNCT", "MERGED", "ACQUIRED", "RENAMED", "DORMANT"],
required=False,
)
# Founding information
founded_year = serializers.IntegerField(required=False, allow_null=True)
founded_date = serializers.DateField(required=False, allow_null=True)
founded_date_precision = serializers.ChoiceField(
choices=["YEAR", "MONTH", "DAY"],
required=False,
allow_blank=True,
)
# Image URLs
logo_url = serializers.URLField(required=False, allow_blank=True)
banner_image_url = serializers.URLField(required=False, allow_blank=True)
card_image_url = serializers.URLField(required=False, allow_blank=True)
# === RIDE MODEL SERIALIZERS ===

View File

@@ -208,6 +208,9 @@ class RideDetailOutputSerializer(serializers.Serializer):
banner_image = serializers.SerializerMethodField()
card_image = serializers.SerializerMethodField()
# Former names (name history)
former_names = serializers.SerializerMethodField()
# URL
url = serializers.SerializerMethodField()
@@ -406,6 +409,24 @@ class RideDetailOutputSerializer(serializers.Serializer):
return None
@extend_schema_field(serializers.ListField(child=serializers.DictField()))
def get_former_names(self, obj):
"""Get the former names (name history) for this ride."""
from apps.rides.models import RideNameHistory
former_names = RideNameHistory.objects.filter(ride=obj).order_by("-to_year", "-from_year")
return [
{
"id": entry.id,
"former_name": entry.former_name,
"from_year": entry.from_year,
"to_year": entry.to_year,
"reason": entry.reason,
}
for entry in former_names
]
class RideImageSettingsInputSerializer(serializers.Serializer):
"""Input serializer for setting ride banner and card images."""
@@ -841,3 +862,37 @@ class RideReviewUpdateInputSerializer(serializers.Serializer):
if value and value > timezone.now().date():
raise serializers.ValidationError("Visit date cannot be in the future")
return value
# === RIDE NAME HISTORY SERIALIZERS ===
class RideNameHistoryOutputSerializer(serializers.Serializer):
"""Output serializer for ride name history (former names)."""
id = serializers.IntegerField()
former_name = serializers.CharField()
from_year = serializers.IntegerField(allow_null=True)
to_year = serializers.IntegerField(allow_null=True)
reason = serializers.CharField()
created_at = serializers.DateTimeField()
class RideNameHistoryCreateInputSerializer(serializers.Serializer):
"""Input serializer for creating ride name history entries."""
former_name = serializers.CharField(max_length=200)
from_year = serializers.IntegerField(required=False, allow_null=True, min_value=1800, max_value=2100)
to_year = serializers.IntegerField(required=False, allow_null=True, min_value=1800, max_value=2100)
reason = serializers.CharField(max_length=500, required=False, allow_blank=True, default="")
def validate(self, attrs):
"""Validate year range."""
from_year = attrs.get("from_year")
to_year = attrs.get("to_year")
if from_year and to_year and from_year > to_year:
raise serializers.ValidationError("From year cannot be after to year")
return attrs

View File

@@ -0,0 +1,251 @@
# Generated by Django 5.2.9 on 2026-01-02 00:10
import apps.core.state_machine.fields
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0026_remove_park_insert_insert_remove_park_update_update_and_more"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="company",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="company",
name="update_update",
),
migrations.AddField(
model_name="company",
name="average_rating",
field=models.DecimalField(
blank=True,
decimal_places=2,
help_text="Average rating from reviews (auto-calculated)",
max_digits=3,
null=True,
),
),
migrations.AddField(
model_name="company",
name="banner_image_url",
field=models.URLField(blank=True, help_text="Banner image for company page header"),
),
migrations.AddField(
model_name="company",
name="card_image_url",
field=models.URLField(blank=True, help_text="Card/thumbnail image for listings"),
),
migrations.AddField(
model_name="company",
name="founded_date",
field=models.DateField(blank=True, help_text="Full founding date if known", null=True),
),
migrations.AddField(
model_name="company",
name="founded_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year only"), ("MONTH", "Month and year"), ("DAY", "Full date")],
help_text="Precision of the founding date",
max_length=10,
),
),
migrations.AddField(
model_name="company",
name="logo_url",
field=models.URLField(blank=True, help_text="Company logo image URL"),
),
migrations.AddField(
model_name="company",
name="person_type",
field=models.CharField(
blank=True,
choices=[
("INDIVIDUAL", "Individual"),
("FIRM", "Firm"),
("ORGANIZATION", "Organization"),
("CORPORATION", "Corporation"),
("PARTNERSHIP", "Partnership"),
("GOVERNMENT", "Government Entity"),
],
help_text="Type of entity (individual, firm, organization, etc.)",
max_length=20,
),
),
migrations.AddField(
model_name="company",
name="review_count",
field=models.PositiveIntegerField(default=0, help_text="Total number of reviews (auto-calculated)"),
),
migrations.AddField(
model_name="company",
name="status",
field=models.CharField(
choices=[
("ACTIVE", "Active"),
("DEFUNCT", "Defunct"),
("MERGED", "Merged"),
("ACQUIRED", "Acquired"),
("RENAMED", "Renamed"),
("DORMANT", "Dormant"),
],
default="ACTIVE",
help_text="Current operational status of the company",
max_length=20,
),
),
migrations.AddField(
model_name="companyevent",
name="average_rating",
field=models.DecimalField(
blank=True,
decimal_places=2,
help_text="Average rating from reviews (auto-calculated)",
max_digits=3,
null=True,
),
),
migrations.AddField(
model_name="companyevent",
name="banner_image_url",
field=models.URLField(blank=True, help_text="Banner image for company page header"),
),
migrations.AddField(
model_name="companyevent",
name="card_image_url",
field=models.URLField(blank=True, help_text="Card/thumbnail image for listings"),
),
migrations.AddField(
model_name="companyevent",
name="founded_date",
field=models.DateField(blank=True, help_text="Full founding date if known", null=True),
),
migrations.AddField(
model_name="companyevent",
name="founded_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year only"), ("MONTH", "Month and year"), ("DAY", "Full date")],
help_text="Precision of the founding date",
max_length=10,
),
),
migrations.AddField(
model_name="companyevent",
name="logo_url",
field=models.URLField(blank=True, help_text="Company logo image URL"),
),
migrations.AddField(
model_name="companyevent",
name="person_type",
field=models.CharField(
blank=True,
choices=[
("INDIVIDUAL", "Individual"),
("FIRM", "Firm"),
("ORGANIZATION", "Organization"),
("CORPORATION", "Corporation"),
("PARTNERSHIP", "Partnership"),
("GOVERNMENT", "Government Entity"),
],
help_text="Type of entity (individual, firm, organization, etc.)",
max_length=20,
),
),
migrations.AddField(
model_name="companyevent",
name="review_count",
field=models.PositiveIntegerField(default=0, help_text="Total number of reviews (auto-calculated)"),
),
migrations.AddField(
model_name="companyevent",
name="status",
field=models.CharField(
choices=[
("ACTIVE", "Active"),
("DEFUNCT", "Defunct"),
("MERGED", "Merged"),
("ACQUIRED", "Acquired"),
("RENAMED", "Renamed"),
("DORMANT", "Dormant"),
],
default="ACTIVE",
help_text="Current operational status of the company",
max_length=20,
),
),
migrations.AlterField(
model_name="park",
name="status",
field=apps.core.state_machine.fields.RichFSMField(
allow_deprecated=False,
choice_group="statuses",
choices=[
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
],
default="OPERATING",
domain="parks",
max_length=20,
),
),
migrations.AlterField(
model_name="parkevent",
name="status",
field=apps.core.state_machine.fields.RichFSMField(
allow_deprecated=False,
choice_group="statuses",
choices=[
("OPERATING", "Operating"),
("CLOSED_TEMP", "Temporarily Closed"),
("CLOSED_PERM", "Permanently Closed"),
("UNDER_CONSTRUCTION", "Under Construction"),
("DEMOLISHED", "Demolished"),
("RELOCATED", "Relocated"),
],
default="OPERATING",
domain="parks",
max_length=20,
),
),
pgtrigger.migrations.AddTrigger(
model_name="company",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "parks_companyevent" ("average_rating", "banner_image_url", "card_image_url", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "id", "logo_url", "name", "parks_count", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "review_count", "rides_count", "roles", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."banner_image_url", NEW."card_image_url", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."id", NEW."logo_url", NEW."name", NEW."parks_count", NEW."person_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."review_count", NEW."rides_count", NEW."roles", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="048dba77acb14b06b8ae12f5f05710f0da71e3fd",
operation="INSERT",
pgid="pgtrigger_insert_insert_35b57",
table="parks_company",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="company",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "parks_companyevent" ("average_rating", "banner_image_url", "card_image_url", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "id", "logo_url", "name", "parks_count", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "review_count", "rides_count", "roles", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."banner_image_url", NEW."card_image_url", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."id", NEW."logo_url", NEW."name", NEW."parks_count", NEW."person_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."review_count", NEW."rides_count", NEW."roles", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="4a93422ca4b79608f941be5baed899d691d88d97",
operation="UPDATE",
pgid="pgtrigger_update_update_d3286",
table="parks_company",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,96 @@
# Generated by Django 5.2.9 on 2026-01-02 02:30
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("parks", "0027_add_company_entity_fields"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="update_update",
),
migrations.AddField(
model_name="park",
name="closing_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the closing date",
max_length=10,
),
),
migrations.AddField(
model_name="park",
name="opening_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the opening date (YEAR for circa dates)",
max_length=10,
),
),
migrations.AddField(
model_name="parkevent",
name="closing_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the closing date",
max_length=10,
),
),
migrations.AddField(
model_name="parkevent",
name="opening_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the opening date (YEAR for circa dates)",
max_length=10,
),
),
pgtrigger.migrations.AddTrigger(
model_name="park",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "parks_parkevent" ("average_rating", "banner_image_id", "card_image_id", "closing_date", "closing_date_precision", "coaster_count", "created_at", "description", "email", "id", "name", "opening_date", "opening_date_precision", "opening_year", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "phone", "property_owner_id", "ride_count", "search_text", "size_acres", "slug", "status", "timezone", "updated_at", "url", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."card_image_id", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_count", NEW."created_at", NEW."description", NEW."email", NEW."id", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."operating_season", NEW."operator_id", NEW."park_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."phone", NEW."property_owner_id", NEW."ride_count", NEW."search_text", NEW."size_acres", NEW."slug", NEW."status", NEW."timezone", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;',
hash="ba8a1efa2a6987e5803856a7d583d15379374976",
operation="INSERT",
pgid="pgtrigger_insert_insert_66883",
table="parks_park",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="park",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "parks_parkevent" ("average_rating", "banner_image_id", "card_image_id", "closing_date", "closing_date_precision", "coaster_count", "created_at", "description", "email", "id", "name", "opening_date", "opening_date_precision", "opening_year", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "phone", "property_owner_id", "ride_count", "search_text", "size_acres", "slug", "status", "timezone", "updated_at", "url", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."card_image_id", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_count", NEW."created_at", NEW."description", NEW."email", NEW."id", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."operating_season", NEW."operator_id", NEW."park_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."phone", NEW."property_owner_id", NEW."ride_count", NEW."search_text", NEW."size_acres", NEW."slug", NEW."status", NEW."timezone", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;',
hash="03e752224a0f76749f4348abf48cb9e6929c9e19",
operation="UPDATE",
pgid="pgtrigger_update_update_19f56",
table="parks_park",
when="AFTER",
),
),
),
]

View File

@@ -14,6 +14,7 @@ class Company(TrackedModel):
objects = CompanyManager()
# Core Fields
name = models.CharField(max_length=255, help_text="Company name")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
roles = ArrayField(
@@ -25,8 +26,72 @@ class Company(TrackedModel):
description = models.TextField(blank=True, help_text="Detailed company description")
website = models.URLField(blank=True, help_text="Company website URL")
# Operator-specific fields
# Person/Entity Type (ported from legacy thrillwiki-87)
PERSON_TYPES = [
("INDIVIDUAL", "Individual"),
("FIRM", "Firm"),
("ORGANIZATION", "Organization"),
("CORPORATION", "Corporation"),
("PARTNERSHIP", "Partnership"),
("GOVERNMENT", "Government Entity"),
]
person_type = models.CharField(
max_length=20,
choices=PERSON_TYPES,
blank=True,
help_text="Type of entity (individual, firm, organization, etc.)",
)
# Company Status (ported from legacy)
COMPANY_STATUSES = [
("ACTIVE", "Active"),
("DEFUNCT", "Defunct"),
("MERGED", "Merged"),
("ACQUIRED", "Acquired"),
("RENAMED", "Renamed"),
("DORMANT", "Dormant"),
]
status = models.CharField(
max_length=20,
choices=COMPANY_STATUSES,
default="ACTIVE",
help_text="Current operational status of the company",
)
# Founding Information (enhanced from just founded_year)
founded_year = models.PositiveIntegerField(blank=True, null=True, help_text="Year the company was founded")
founded_date = models.DateField(blank=True, null=True, help_text="Full founding date if known")
DATE_PRECISION_CHOICES = [
("YEAR", "Year only"),
("MONTH", "Month and year"),
("DAY", "Full date"),
]
founded_date_precision = models.CharField(
max_length=10,
choices=DATE_PRECISION_CHOICES,
blank=True,
help_text="Precision of the founding date",
)
# Image URLs (ported from legacy)
logo_url = models.URLField(blank=True, help_text="Company logo image URL")
banner_image_url = models.URLField(blank=True, help_text="Banner image for company page header")
card_image_url = models.URLField(blank=True, help_text="Card/thumbnail image for listings")
# Rating & Review Aggregates (computed fields, updated by triggers/signals)
average_rating = models.DecimalField(
max_digits=3,
decimal_places=2,
blank=True,
null=True,
help_text="Average rating from reviews (auto-calculated)",
)
review_count = models.PositiveIntegerField(
default=0,
help_text="Total number of reviews (auto-calculated)",
)
# Counts (auto-calculated)
parks_count = models.IntegerField(default=0, help_text="Number of parks operated (auto-calculated)")
rides_count = models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)")

View File

@@ -54,7 +54,21 @@ class Park(StateMachineMixin, TrackedModel):
# Details
opening_date = models.DateField(null=True, blank=True, help_text="Opening date")
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 (YEAR for circa dates)",
)
closing_date = models.DateField(null=True, blank=True, help_text="Closing date")
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",
)
operating_season = models.CharField(max_length=255, blank=True, help_text="Operating season")
size_acres = models.DecimalField(
max_digits=10, decimal_places=2, null=True, blank=True, help_text="Park size in acres"
@@ -310,6 +324,14 @@ class Park(StateMachineMixin, TrackedModel):
return self.location.formatted_address
return ""
@property
def is_closing(self) -> bool:
"""Returns True if this park 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
@property
def coordinates(self) -> list[float] | None:
"""Returns coordinates as a list [latitude, longitude]"""

View File

@@ -0,0 +1,454 @@
# Generated by Django 5.2.9 on 2026-01-01 21:25
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pghistory", "0007_auto_20250421_0444"),
("rides", "0029_darkridestats_darkridestatsevent_flatridestats_and_more"),
]
operations = [
migrations.CreateModel(
name="KiddieRideStats",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"min_age",
models.PositiveIntegerField(blank=True, help_text="Minimum recommended age in years", null=True),
),
(
"max_age",
models.PositiveIntegerField(blank=True, help_text="Maximum recommended age in years", null=True),
),
(
"educational_theme",
models.CharField(
blank=True,
help_text="Educational theme if applicable (e.g., 'Dinosaurs', 'Space')",
max_length=200,
),
),
(
"character_theme",
models.CharField(
blank=True,
help_text="Character theme if applicable (e.g., 'Paw Patrol', 'Peppa Pig')",
max_length=200,
),
),
(
"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(blank=True, help_text="Number of seats per ride vehicle", null=True),
),
],
options={
"verbose_name": "Kiddie Ride Statistics",
"verbose_name_plural": "Kiddie Ride Statistics",
"ordering": ["ride"],
"abstract": False,
},
),
migrations.CreateModel(
name="KiddieRideStatsEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"min_age",
models.PositiveIntegerField(blank=True, help_text="Minimum recommended age in years", null=True),
),
(
"max_age",
models.PositiveIntegerField(blank=True, help_text="Maximum recommended age in years", null=True),
),
(
"educational_theme",
models.CharField(
blank=True,
help_text="Educational theme if applicable (e.g., 'Dinosaurs', 'Space')",
max_length=200,
),
),
(
"character_theme",
models.CharField(
blank=True,
help_text="Character theme if applicable (e.g., 'Paw Patrol', 'Peppa Pig')",
max_length=200,
),
),
(
"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(blank=True, help_text="Number of seats per ride vehicle", null=True),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="TransportationStats",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"transport_type",
models.CharField(
choices=[
("TRAIN", "Train"),
("MONORAIL", "Monorail"),
("SKYLIFT", "Skylift / Chairlift"),
("FERRY", "Ferry / Boat"),
("PEOPLEMOVER", "PeopleMover"),
("CABLE_CAR", "Cable Car"),
("TRAM", "Tram"),
],
default="TRAIN",
help_text="Type of transportation",
max_length=20,
),
),
(
"route_length_ft",
models.DecimalField(
blank=True, decimal_places=2, help_text="Total route length in feet", max_digits=8, null=True
),
),
(
"stations_count",
models.PositiveIntegerField(
blank=True, help_text="Number of stations/stops on the route", null=True
),
),
(
"vehicle_capacity",
models.PositiveIntegerField(blank=True, help_text="Passenger capacity per vehicle", null=True),
),
(
"vehicles_count",
models.PositiveIntegerField(
blank=True, help_text="Total number of vehicles in operation", null=True
),
),
(
"round_trip_duration_minutes",
models.PositiveIntegerField(
blank=True, help_text="Duration of a complete round trip in minutes", null=True
),
),
(
"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)"
),
),
],
options={
"verbose_name": "Transportation Statistics",
"verbose_name_plural": "Transportation Statistics",
"ordering": ["ride"],
"abstract": False,
},
),
migrations.CreateModel(
name="TransportationStatsEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"transport_type",
models.CharField(
choices=[
("TRAIN", "Train"),
("MONORAIL", "Monorail"),
("SKYLIFT", "Skylift / Chairlift"),
("FERRY", "Ferry / Boat"),
("PEOPLEMOVER", "PeopleMover"),
("CABLE_CAR", "Cable Car"),
("TRAM", "Tram"),
],
default="TRAIN",
help_text="Type of transportation",
max_length=20,
),
),
(
"route_length_ft",
models.DecimalField(
blank=True, decimal_places=2, help_text="Total route length in feet", max_digits=8, null=True
),
),
(
"stations_count",
models.PositiveIntegerField(
blank=True, help_text="Number of stations/stops on the route", null=True
),
),
(
"vehicle_capacity",
models.PositiveIntegerField(blank=True, help_text="Passenger capacity per vehicle", null=True),
),
(
"vehicles_count",
models.PositiveIntegerField(
blank=True, help_text="Total number of vehicles in operation", null=True
),
),
(
"round_trip_duration_minutes",
models.PositiveIntegerField(
blank=True, help_text="Duration of a complete round trip in minutes", null=True
),
),
(
"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)"
),
),
],
options={
"abstract": False,
},
),
migrations.AlterModelOptions(
name="ridecredit",
options={
"ordering": ["display_order", "-last_ridden_at", "-first_ridden_at", "-created_at"],
"verbose_name": "Ride Credit",
"verbose_name_plural": "Ride Credits",
},
),
pgtrigger.migrations.RemoveTrigger(
model_name="ridecredit",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="ridecredit",
name="update_update",
),
migrations.AddField(
model_name="ridecredit",
name="display_order",
field=models.PositiveIntegerField(default=0, help_text="User-defined display order for drag-drop sorting"),
),
migrations.AddField(
model_name="ridecreditevent",
name="display_order",
field=models.PositiveIntegerField(default=0, help_text="User-defined display order for drag-drop sorting"),
),
pgtrigger.migrations.AddTrigger(
model_name="ridecredit",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_ridecreditevent" ("count", "created_at", "display_order", "first_ridden_at", "id", "last_ridden_at", "notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "ride_id", "updated_at", "user_id") VALUES (NEW."count", NEW."created_at", NEW."display_order", NEW."first_ridden_at", NEW."id", NEW."last_ridden_at", NEW."notes", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rating", NEW."ride_id", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="680f93dab99a404aea8f73f8328eff04cd561254",
operation="INSERT",
pgid="pgtrigger_insert_insert_00439",
table="rides_ridecredit",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="ridecredit",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_ridecreditevent" ("count", "created_at", "display_order", "first_ridden_at", "id", "last_ridden_at", "notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "ride_id", "updated_at", "user_id") VALUES (NEW."count", NEW."created_at", NEW."display_order", NEW."first_ridden_at", NEW."id", NEW."last_ridden_at", NEW."notes", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rating", NEW."ride_id", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="e9889a572acd9261c1355ab47458f3eaf2b07c13",
operation="UPDATE",
pgid="pgtrigger_update_update_32a65",
table="rides_ridecredit",
when="AFTER",
),
),
),
migrations.AddField(
model_name="kiddieridestats",
name="ride",
field=models.OneToOneField(
help_text="Ride these kiddie ride statistics belong to",
on_delete=django.db.models.deletion.CASCADE,
related_name="kiddie_stats",
to="rides.ride",
),
),
migrations.AddField(
model_name="kiddieridestatsevent",
name="pgh_context",
field=models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
migrations.AddField(
model_name="kiddieridestatsevent",
name="pgh_obj",
field=models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="rides.kiddieridestats",
),
),
migrations.AddField(
model_name="kiddieridestatsevent",
name="ride",
field=models.ForeignKey(
db_constraint=False,
help_text="Ride these kiddie ride statistics belong to",
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="rides.ride",
),
),
migrations.AddField(
model_name="transportationstats",
name="ride",
field=models.OneToOneField(
help_text="Ride these transportation statistics belong to",
on_delete=django.db.models.deletion.CASCADE,
related_name="transport_stats",
to="rides.ride",
),
),
migrations.AddField(
model_name="transportationstatsevent",
name="pgh_context",
field=models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
migrations.AddField(
model_name="transportationstatsevent",
name="pgh_obj",
field=models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="rides.transportationstats",
),
),
migrations.AddField(
model_name="transportationstatsevent",
name="ride",
field=models.ForeignKey(
db_constraint=False,
help_text="Ride these transportation statistics belong to",
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="rides.ride",
),
),
pgtrigger.migrations.AddTrigger(
model_name="kiddieridestats",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_kiddieridestatsevent" ("adult_ride_along", "character_theme", "created_at", "educational_theme", "guardian_required", "id", "max_age", "min_age", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_id", "seats_per_vehicle", "updated_at") VALUES (NEW."adult_ride_along", NEW."character_theme", NEW."created_at", NEW."educational_theme", NEW."guardian_required", NEW."id", NEW."max_age", NEW."min_age", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."ride_id", NEW."seats_per_vehicle", NEW."updated_at"); RETURN NULL;',
hash="b5d181566e5d0710c5b4093a5b61dc54591e5639",
operation="INSERT",
pgid="pgtrigger_insert_insert_9949f",
table="rides_kiddieridestats",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="kiddieridestats",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_kiddieridestatsevent" ("adult_ride_along", "character_theme", "created_at", "educational_theme", "guardian_required", "id", "max_age", "min_age", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_id", "seats_per_vehicle", "updated_at") VALUES (NEW."adult_ride_along", NEW."character_theme", NEW."created_at", NEW."educational_theme", NEW."guardian_required", NEW."id", NEW."max_age", NEW."min_age", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."ride_id", NEW."seats_per_vehicle", NEW."updated_at"); RETURN NULL;',
hash="672bb3e42cda03094d9300b6e0d9d89b2797bd05",
operation="UPDATE",
pgid="pgtrigger_update_update_fb19d",
table="rides_kiddieridestats",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="transportationstats",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_transportationstatsevent" ("created_at", "id", "is_one_way", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_id", "round_trip_duration_minutes", "route_length_ft", "scenic_highlights", "stations_count", "transport_type", "updated_at", "vehicle_capacity", "vehicles_count") VALUES (NEW."created_at", NEW."id", NEW."is_one_way", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."ride_id", NEW."round_trip_duration_minutes", NEW."route_length_ft", NEW."scenic_highlights", NEW."stations_count", NEW."transport_type", NEW."updated_at", NEW."vehicle_capacity", NEW."vehicles_count"); RETURN NULL;',
hash="95a70a6e71eb5ea32726a19e5eb6286c20b19952",
operation="INSERT",
pgid="pgtrigger_insert_insert_c811e",
table="rides_transportationstats",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="transportationstats",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_transportationstatsevent" ("created_at", "id", "is_one_way", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_id", "round_trip_duration_minutes", "route_length_ft", "scenic_highlights", "stations_count", "transport_type", "updated_at", "vehicle_capacity", "vehicles_count") VALUES (NEW."created_at", NEW."id", NEW."is_one_way", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."ride_id", NEW."round_trip_duration_minutes", NEW."route_length_ft", NEW."scenic_highlights", NEW."stations_count", NEW."transport_type", NEW."updated_at", NEW."vehicle_capacity", NEW."vehicles_count"); RETURN NULL;',
hash="901b16a5411e78635442895d7107b298634d25c3",
operation="UPDATE",
pgid="pgtrigger_update_update_ccccf",
table="rides_transportationstats",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,155 @@
# Generated by Django 5.2.9 on 2026-01-02 00:33
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pghistory", "0007_auto_20250421_0444"),
("rides", "0030_add_kiddie_and_transportation_stats"),
]
operations = [
migrations.CreateModel(
name="RideNameHistory",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("former_name", models.CharField(help_text="The previous name of the ride", max_length=200)),
(
"from_year",
models.PositiveSmallIntegerField(
blank=True, help_text="Year the ride started using this name", null=True
),
),
(
"to_year",
models.PositiveSmallIntegerField(
blank=True, help_text="Year the ride stopped using this name", null=True
),
),
(
"reason",
models.CharField(
blank=True, help_text="Reason for the name change (e.g., 'Retheme to Peanuts')", max_length=500
),
),
(
"ride",
models.ForeignKey(
help_text="The ride this name history entry belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="former_names",
to="rides.ride",
),
),
],
options={
"verbose_name": "Ride Name History",
"verbose_name_plural": "Ride Name Histories",
"ordering": ["-to_year", "-from_year"],
"abstract": False,
},
),
migrations.CreateModel(
name="RideNameHistoryEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("former_name", models.CharField(help_text="The previous name of the ride", max_length=200)),
(
"from_year",
models.PositiveSmallIntegerField(
blank=True, help_text="Year the ride started using this name", null=True
),
),
(
"to_year",
models.PositiveSmallIntegerField(
blank=True, help_text="Year the ride stopped using this name", null=True
),
),
(
"reason",
models.CharField(
blank=True, help_text="Reason for the name change (e.g., 'Retheme to Peanuts')", max_length=500
),
),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="rides.ridenamehistory",
),
),
(
"ride",
models.ForeignKey(
db_constraint=False,
help_text="The ride this name history entry belongs to",
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="rides.ride",
),
),
],
options={
"abstract": False,
},
),
migrations.AddIndex(
model_name="ridenamehistory",
index=models.Index(fields=["ride", "-to_year"], name="rides_riden_ride_id_b546e5_idx"),
),
pgtrigger.migrations.AddTrigger(
model_name="ridenamehistory",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_ridenamehistoryevent" ("created_at", "former_name", "from_year", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reason", "ride_id", "to_year", "updated_at") VALUES (NEW."created_at", NEW."former_name", NEW."from_year", NEW."id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."reason", NEW."ride_id", NEW."to_year", NEW."updated_at"); RETURN NULL;',
hash="b79231914244e431999014db94fd52759cc41541",
operation="INSERT",
pgid="pgtrigger_insert_insert_ca1f7",
table="rides_ridenamehistory",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="ridenamehistory",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_ridenamehistoryevent" ("created_at", "former_name", "from_year", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reason", "ride_id", "to_year", "updated_at") VALUES (NEW."created_at", NEW."former_name", NEW."from_year", NEW."id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."reason", NEW."ride_id", NEW."to_year", NEW."updated_at"); RETURN NULL;',
hash="c760d29ecb57f1f3c92e76c7f5d45027db136b84",
operation="UPDATE",
pgid="pgtrigger_update_update_99e4e",
table="rides_ridenamehistory",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,96 @@
# Generated by Django 5.2.9 on 2026-01-02 02:30
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rides", "0031_add_ride_name_history"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="ride",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="ride",
name="update_update",
),
migrations.AddField(
model_name="ride",
name="closing_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the closing date",
max_length=10,
),
),
migrations.AddField(
model_name="ride",
name="opening_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the opening date",
max_length=10,
),
),
migrations.AddField(
model_name="rideevent",
name="closing_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the closing date",
max_length=10,
),
),
migrations.AddField(
model_name="rideevent",
name="opening_date_precision",
field=models.CharField(
blank=True,
choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")],
default="DAY",
help_text="Precision of the opening date",
max_length=10,
),
),
pgtrigger.migrations.AddTrigger(
model_name="ride",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_rideevent" ("average_rating", "banner_image_id", "capacity_per_hour", "card_image_id", "category", "closing_date", "closing_date_precision", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "search_text", "slug", "status", "status_since", "updated_at", "url") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."closing_date", NEW."closing_date_precision", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."search_text", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at", NEW."url"); RETURN NULL;',
hash="e0a64190a51762d71e695238a7ee9feedb95fd41",
operation="INSERT",
pgid="pgtrigger_insert_insert_52074",
table="rides_ride",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="ride",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_rideevent" ("average_rating", "banner_image_id", "capacity_per_hour", "card_image_id", "category", "closing_date", "closing_date_precision", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "search_text", "slug", "status", "status_since", "updated_at", "url") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."closing_date", NEW."closing_date_precision", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."search_text", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at", NEW."url"); RETURN NULL;',
hash="e3991e794f1239191cfe9095bab207527123b94f",
operation="UPDATE",
pgid="pgtrigger_update_update_4917a",
table="rides_ride",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,84 @@
# Generated by Django 5.2.9 on 2026-01-02 02:36
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("rides", "0032_add_date_precision_fields"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="ride",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="ride",
name="update_update",
),
migrations.AddField(
model_name="ride",
name="age_requirement",
field=models.PositiveIntegerField(
blank=True, help_text="Minimum age requirement in years (if any)", null=True
),
),
migrations.AddField(
model_name="ride",
name="ride_sub_type",
field=models.CharField(
blank=True,
help_text="Sub-category of ride (e.g., 'Flying Coaster', 'Inverted Coaster', 'Log Flume')",
max_length=100,
),
),
migrations.AddField(
model_name="rideevent",
name="age_requirement",
field=models.PositiveIntegerField(
blank=True, help_text="Minimum age requirement in years (if any)", null=True
),
),
migrations.AddField(
model_name="rideevent",
name="ride_sub_type",
field=models.CharField(
blank=True,
help_text="Sub-category of ride (e.g., 'Flying Coaster', 'Inverted Coaster', 'Log Flume')",
max_length=100,
),
),
pgtrigger.migrations.AddTrigger(
model_name="ride",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "rides_rideevent" ("age_requirement", "average_rating", "banner_image_id", "capacity_per_hour", "card_image_id", "category", "closing_date", "closing_date_precision", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "search_text", "slug", "status", "status_since", "updated_at", "url") VALUES (NEW."age_requirement", NEW."average_rating", NEW."banner_image_id", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."closing_date", NEW."closing_date_precision", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."search_text", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at", NEW."url"); RETURN NULL;',
hash="cd829a8030511234c62ed355a58c753d15d09df9",
operation="INSERT",
pgid="pgtrigger_insert_insert_52074",
table="rides_ride",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="ride",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "rides_rideevent" ("age_requirement", "average_rating", "banner_image_id", "capacity_per_hour", "card_image_id", "category", "closing_date", "closing_date_precision", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "search_text", "slug", "status", "status_since", "updated_at", "url") VALUES (NEW."age_requirement", NEW."average_rating", NEW."banner_image_id", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."closing_date", NEW."closing_date_precision", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."search_text", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at", NEW."url"); RETURN NULL;',
hash="32d2f4a4547c7fd91e136ea0ac378f1b6bb8ef30",
operation="UPDATE",
pgid="pgtrigger_update_update_4917a",
table="rides_ride",
when="AFTER",
),
),
),
]

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}"

View File

@@ -235,3 +235,46 @@ def update_ride_search_text_on_ride_model_change(sender, instance, **kwargs):
update_ride_search_text(ride)
except Exception as e:
logger.exception(f"Failed to update ride search_text on ride model change: {e}")
# =============================================================================
# Automatic Name History Tracking
# =============================================================================
@receiver(pre_save, sender=Ride)
def track_ride_name_changes(sender, instance, **kwargs):
"""
Automatically create RideNameHistory when a ride's name changes.
This ensures versioning is automatic - when a ride is renamed,
the previous name is preserved in the name history.
"""
if not instance.pk:
return # Skip new rides
try:
old_instance = Ride.objects.get(pk=instance.pk)
if old_instance.name != instance.name:
from .models import RideNameHistory
current_year = timezone.now().year
# Create history entry for the old name
RideNameHistory.objects.create(
ride=instance,
former_name=old_instance.name,
to_year=current_year,
reason="Name changed",
)
logger.info(
f"Ride {instance.pk} name changed from '{old_instance.name}' "
f"to '{instance.name}' - history entry created"
)
except Ride.DoesNotExist:
pass # New ride, no history to track
except Exception as e:
logger.exception(f"Failed to track name change for ride {instance.pk}: {e}")