mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 02:15:19 -05:00
lol
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
251
backend/apps/parks/migrations/0027_add_company_entity_fields.py
Normal file
251
backend/apps/parks/migrations/0027_add_company_entity_fields.py
Normal 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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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)")
|
||||
|
||||
|
||||
@@ -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]"""
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
155
backend/apps/rides/migrations/0031_add_ride_name_history.py
Normal file
155
backend/apps/rides/migrations/0031_add_ride_name_history.py
Normal 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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
73
backend/apps/rides/models/name_history.py
Normal file
73
backend/apps/rides/models/name_history.py
Normal 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."}
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user