diff --git a/.replit b/.replit index ffaf0d83..93264b02 100644 --- a/.replit +++ b/.replit @@ -34,3 +34,7 @@ outputType = "webview" [[ports]] localPort = 5000 externalPort = 80 + +[[ports]] +localPort = 32787 +externalPort = 3000 diff --git a/backend/apps/accounts/migrations/0001_initial.py b/backend/apps/accounts/migrations/0001_initial.py index f4e6a311..e0d0869e 100644 --- a/backend/apps/accounts/migrations/0001_initial.py +++ b/backend/apps/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.6 on 2025-09-21 01:19 +# Generated by Django 5.2.6 on 2025-09-21 01:29 import apps.core.choices.fields import django.contrib.auth.models diff --git a/backend/apps/accounts/migrations/0002_remove_userprofile_insert_insert_and_more.py b/backend/apps/accounts/migrations/0002_remove_userprofile_insert_insert_and_more.py new file mode 100644 index 00000000..bb4645a2 --- /dev/null +++ b/backend/apps/accounts/migrations/0002_remove_userprofile_insert_insert_and_more.py @@ -0,0 +1,77 @@ +# Generated by Django 5.2.6 on 2025-09-21 01:29 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0001_initial"), + ("django_cloudflareimages_toolkit", "0001_initial"), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name="userprofile", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="userprofile", + name="update_update", + ), + migrations.AddField( + model_name="userprofile", + name="avatar", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + migrations.AddField( + model_name="userprofileevent", + name="avatar", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="userprofile", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "accounts_userprofileevent" ("avatar_id", "bio", "coaster_credits", "dark_ride_credits", "discord", "display_name", "flat_ride_credits", "id", "instagram", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "profile_id", "pronouns", "twitter", "user_id", "water_ride_credits", "youtube") VALUES (NEW."avatar_id", NEW."bio", NEW."coaster_credits", NEW."dark_ride_credits", NEW."discord", NEW."display_name", NEW."flat_ride_credits", NEW."id", NEW."instagram", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."profile_id", NEW."pronouns", NEW."twitter", NEW."user_id", NEW."water_ride_credits", NEW."youtube"); RETURN NULL;', + hash="a7ecdb1ac2821dea1fef4ec917eeaf6b8e4f09c8", + operation="INSERT", + pgid="pgtrigger_insert_insert_c09d7", + table="accounts_userprofile", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="userprofile", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "accounts_userprofileevent" ("avatar_id", "bio", "coaster_credits", "dark_ride_credits", "discord", "display_name", "flat_ride_credits", "id", "instagram", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "profile_id", "pronouns", "twitter", "user_id", "water_ride_credits", "youtube") VALUES (NEW."avatar_id", NEW."bio", NEW."coaster_credits", NEW."dark_ride_credits", NEW."discord", NEW."display_name", NEW."flat_ride_credits", NEW."id", NEW."instagram", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."profile_id", NEW."pronouns", NEW."twitter", NEW."user_id", NEW."water_ride_credits", NEW."youtube"); RETURN NULL;', + hash="81607e492ffea2a4c741452b860ee660374cc01d", + operation="UPDATE", + pgid="pgtrigger_update_update_87ef6", + table="accounts_userprofile", + when="AFTER", + ), + ), + ), + ] diff --git a/backend/apps/accounts/models.py b/backend/apps/accounts/models.py index 2230d49d..b31f569b 100644 --- a/backend/apps/accounts/models.py +++ b/backend/apps/accounts/models.py @@ -149,12 +149,12 @@ class UserProfile(models.Model): blank=True, help_text="Legacy display name field - use User.display_name instead", ) - # avatar = models.ForeignKey( - # 'django_cloudflareimages_toolkit.CloudflareImage', - # on_delete=models.SET_NULL, - # null=True, - # blank=True - # ) + avatar = models.ForeignKey( + 'django_cloudflareimages_toolkit.CloudflareImage', + on_delete=models.SET_NULL, + null=True, + blank=True + ) pronouns = models.CharField(max_length=50, blank=True) bio = models.TextField(max_length=500, blank=True) @@ -175,27 +175,26 @@ class UserProfile(models.Model): """ Return the avatar URL or generate a default letter-based avatar URL """ - # Avatar field temporarily disabled - return default avatar - # if self.avatar and self.avatar.is_uploaded: - # # Try to get avatar variant first, fallback to public - # avatar_url = self.avatar.get_url('avatar') - # if avatar_url: - # return avatar_url + if self.avatar and self.avatar.is_uploaded: + # Try to get avatar variant first, fallback to public + avatar_url = self.avatar.get_url('avatar') + if avatar_url: + return avatar_url - # # Fallback to public variant - # public_url = self.avatar.get_url('public') - # if public_url: - # return public_url + # Fallback to public variant + public_url = self.avatar.get_url('public') + if public_url: + return public_url - # # Last fallback - try any available variant - # if self.avatar.variants: - # if isinstance(self.avatar.variants, list) and self.avatar.variants: - # return self.avatar.variants[0] - # elif isinstance(self.avatar.variants, dict): - # # Return first available variant - # for variant_url in self.avatar.variants.values(): - # if variant_url: - # return variant_url + # Last fallback - try any available variant + if self.avatar.variants: + if isinstance(self.avatar.variants, list) and self.avatar.variants: + return self.avatar.variants[0] + elif isinstance(self.avatar.variants, dict): + # Return first available variant + for variant_url in self.avatar.variants.values(): + if variant_url: + return variant_url # Generate default letter-based avatar using first letter of username first_letter = self.user.username[0].upper() if self.user.username else "U" @@ -206,33 +205,32 @@ class UserProfile(models.Model): """ Return avatar variants for different use cases """ - # Avatar field temporarily disabled - return default variants - # if self.avatar and self.avatar.is_uploaded: - # variants = {} + if self.avatar and self.avatar.is_uploaded: + variants = {} - # # Try to get specific variants - # thumbnail_url = self.avatar.get_url('thumbnail') - # avatar_url = self.avatar.get_url('avatar') - # large_url = self.avatar.get_url('large') - # public_url = self.avatar.get_url('public') + # Try to get specific variants + thumbnail_url = self.avatar.get_url('thumbnail') + avatar_url = self.avatar.get_url('avatar') + large_url = self.avatar.get_url('large') + public_url = self.avatar.get_url('public') - # # Use specific variants if available, otherwise fallback to public or first available - # fallback_url = public_url - # if not fallback_url and self.avatar.variants: - # if isinstance(self.avatar.variants, list) and self.avatar.variants: - # fallback_url = self.avatar.variants[0] - # elif isinstance(self.avatar.variants, dict): - # fallback_url = next(iter(self.avatar.variants.values()), None) + # Use specific variants if available, otherwise fallback to public or first available + fallback_url = public_url + if not fallback_url and self.avatar.variants: + if isinstance(self.avatar.variants, list) and self.avatar.variants: + fallback_url = self.avatar.variants[0] + elif isinstance(self.avatar.variants, dict): + fallback_url = next(iter(self.avatar.variants.values()), None) - # variants = { - # "thumbnail": thumbnail_url or fallback_url, - # "avatar": avatar_url or fallback_url, - # "large": large_url or fallback_url, - # } + variants = { + "thumbnail": thumbnail_url or fallback_url, + "avatar": avatar_url or fallback_url, + "large": large_url or fallback_url, + } - # # Only return variants if we have at least one valid URL - # if any(variants.values()): - # return variants + # Only return variants if we have at least one valid URL + if any(variants.values()): + return variants # For default avatars, return the same URL for all variants default_url = self.get_avatar_url() diff --git a/backend/apps/core/migrations/0001_initial.py b/backend/apps/core/migrations/0001_initial.py new file mode 100644 index 00000000..06325f2b --- /dev/null +++ b/backend/apps/core/migrations/0001_initial.py @@ -0,0 +1,292 @@ +# Generated by Django 5.2.6 on 2025-09-21 01:27 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("pghistory", "0007_auto_20250421_0444"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="PageView", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("object_id", models.PositiveIntegerField()), + ("timestamp", models.DateTimeField(auto_now_add=True, db_index=True)), + ("ip_address", models.GenericIPAddressField()), + ("user_agent", models.CharField(blank=True, max_length=512)), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_views", + to="contenttypes.contenttype", + ), + ), + ], + ), + migrations.CreateModel( + name="PageViewEvent", + 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()), + ("object_id", models.PositiveIntegerField()), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ("ip_address", models.GenericIPAddressField()), + ("user_agent", models.CharField(blank=True, max_length=512)), + ( + "content_type", + models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="contenttypes.contenttype", + ), + ), + ( + "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="core.pageview", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="SlugHistory", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("object_id", models.CharField(max_length=50)), + ("old_slug", models.SlugField(max_length=200)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.contenttype", + ), + ), + ], + options={ + "verbose_name_plural": "Slug histories", + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="SlugHistoryEvent", + 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()), + ("object_id", models.CharField(max_length=50)), + ("old_slug", models.SlugField(db_index=False, max_length=200)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "content_type", + models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="contenttypes.contenttype", + ), + ), + ( + "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="core.slughistory", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="HistoricalSlug", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("object_id", models.PositiveIntegerField()), + ("slug", models.SlugField(max_length=255)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.contenttype", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="historical_slugs", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "indexes": [ + models.Index( + fields=["content_type", "object_id"], + name="core_histor_content_b4c470_idx", + ), + models.Index(fields=["slug"], name="core_histor_slug_8fd7b3_idx"), + ], + "unique_together": {("content_type", "slug")}, + }, + ), + migrations.AddIndex( + model_name="pageview", + index=models.Index( + fields=["timestamp"], name="core_pagevi_timesta_757ebb_idx" + ), + ), + migrations.AddIndex( + model_name="pageview", + index=models.Index( + fields=["content_type", "object_id"], + name="core_pagevi_content_eda7ad_idx", + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="pageview", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "core_pageviewevent" ("content_type_id", "id", "ip_address", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "timestamp", "user_agent") VALUES (NEW."content_type_id", NEW."id", NEW."ip_address", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."timestamp", NEW."user_agent"); RETURN NULL;', + hash="1682d124ea3ba215e630c7cfcde929f7444cf247", + operation="INSERT", + pgid="pgtrigger_insert_insert_ee1e1", + table="core_pageview", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="pageview", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "core_pageviewevent" ("content_type_id", "id", "ip_address", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "timestamp", "user_agent") VALUES (NEW."content_type_id", NEW."id", NEW."ip_address", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."timestamp", NEW."user_agent"); RETURN NULL;', + hash="4221b2dd6636cae454f8d69c0c1841c40c47e6a6", + operation="UPDATE", + pgid="pgtrigger_update_update_3c505", + table="core_pageview", + when="AFTER", + ), + ), + ), + migrations.AddIndex( + model_name="slughistory", + index=models.Index( + fields=["content_type", "object_id"], + name="core_slughi_content_8bbf56_idx", + ), + ), + migrations.AddIndex( + model_name="slughistory", + index=models.Index( + fields=["old_slug"], name="core_slughi_old_slu_aaef7f_idx" + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="slughistory", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "core_slughistoryevent" ("content_type_id", "created_at", "id", "object_id", "old_slug", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id") VALUES (NEW."content_type_id", NEW."created_at", NEW."id", NEW."object_id", NEW."old_slug", _pgh_attach_context(), NOW(), \'insert\', NEW."id"); RETURN NULL;', + hash="2a2a05025693c165b88e5eba7fcc23214749a78b", + operation="INSERT", + pgid="pgtrigger_insert_insert_3002a", + table="core_slughistory", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="slughistory", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "core_slughistoryevent" ("content_type_id", "created_at", "id", "object_id", "old_slug", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id") VALUES (NEW."content_type_id", NEW."created_at", NEW."id", NEW."object_id", NEW."old_slug", _pgh_attach_context(), NOW(), \'update\', NEW."id"); RETURN NULL;', + hash="3ad197ccb6178668e762720341e45d3fd3216776", + operation="UPDATE", + pgid="pgtrigger_update_update_52030", + table="core_slughistory", + when="AFTER", + ), + ), + ), + ] diff --git a/backend/apps/core/migrations/__init__.py b/backend/apps/core/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/parks/migrations/0001_initial.py b/backend/apps/parks/migrations/0001_initial.py new file mode 100644 index 00000000..b0f6c45b --- /dev/null +++ b/backend/apps/parks/migrations/0001_initial.py @@ -0,0 +1,1475 @@ +# Generated by Django 5.2.6 on 2025-09-21 01:27 + +import apps.core.choices.fields +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +import django.db.models.functions.datetime +import pgtrigger.compiler +import pgtrigger.migrations +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("django_cloudflareimages_toolkit", "__first__"), + ("pghistory", "0007_auto_20250421_0444"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Company", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "roles", + django.contrib.postgres.fields.ArrayField( + base_field=apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="company_roles", + choices=[ + ("OPERATOR", "Park Operator"), + ("PROPERTY_OWNER", "Property Owner"), + ], + domain="parks", + max_length=20, + ), + blank=True, + default=list, + size=None, + ), + ), + ("description", models.TextField(blank=True)), + ("website", models.URLField(blank=True)), + ("founded_year", models.PositiveIntegerField(blank=True, null=True)), + ("parks_count", models.IntegerField(default=0)), + ("rides_count", models.IntegerField(default=0)), + ], + options={ + "verbose_name_plural": "Companies", + "ordering": ["name"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="CompanyEvent", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ( + "roles", + django.contrib.postgres.fields.ArrayField( + base_field=apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="company_roles", + choices=[ + ("OPERATOR", "Park Operator"), + ("PROPERTY_OWNER", "Property Owner"), + ], + domain="parks", + max_length=20, + ), + blank=True, + default=list, + size=None, + ), + ), + ("description", models.TextField(blank=True)), + ("website", models.URLField(blank=True)), + ("founded_year", models.PositiveIntegerField(blank=True, null=True)), + ("parks_count", models.IntegerField(default=0)), + ("rides_count", models.IntegerField(default=0)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="CompanyHeadquarters", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "street_address", + models.CharField( + blank=True, + help_text="Mailing address if publicly available", + max_length=255, + ), + ), + ( + "city", + models.CharField( + db_index=True, help_text="Headquarters city", max_length=100 + ), + ), + ( + "state_province", + models.CharField( + blank=True, + db_index=True, + help_text="State/Province/Region", + max_length=100, + ), + ), + ( + "country", + models.CharField( + db_index=True, + default="USA", + help_text="Country where headquarters is located", + max_length=100, + ), + ), + ( + "postal_code", + models.CharField( + blank=True, help_text="ZIP or postal code", max_length=20 + ), + ), + ( + "mailing_address", + models.TextField( + blank=True, + help_text="Complete mailing address if different from basic address", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Company Headquarters", + "verbose_name_plural": "Company Headquarters", + "ordering": ["company__name"], + }, + ), + migrations.CreateModel( + name="CompanyHeadquartersEvent", + 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()), + ( + "street_address", + models.CharField( + blank=True, + help_text="Mailing address if publicly available", + max_length=255, + ), + ), + ( + "city", + models.CharField(help_text="Headquarters city", max_length=100), + ), + ( + "state_province", + models.CharField( + blank=True, help_text="State/Province/Region", max_length=100 + ), + ), + ( + "country", + models.CharField( + default="USA", + help_text="Country where headquarters is located", + max_length=100, + ), + ), + ( + "postal_code", + models.CharField( + blank=True, help_text="ZIP or postal code", max_length=20 + ), + ), + ( + "mailing_address", + models.TextField( + blank=True, + help_text="Complete mailing address if different from basic address", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Park", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255, unique=True)), + ("description", models.TextField(blank=True)), + ( + "status", + apps.core.choices.fields.RichChoiceField( + 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, + ), + ), + ( + "park_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="types", + choices=[ + ("THEME_PARK", "Theme Park"), + ("AMUSEMENT_PARK", "Amusement Park"), + ("WATER_PARK", "Water Park"), + ( + "FAMILY_ENTERTAINMENT_CENTER", + "Family Entertainment Center", + ), + ("CARNIVAL", "Carnival"), + ("FAIR", "Fair"), + ("PIER", "Pier"), + ("BOARDWALK", "Boardwalk"), + ("SAFARI_PARK", "Safari Park"), + ("ZOO", "Zoo"), + ("OTHER", "Other"), + ], + db_index=True, + default="THEME_PARK", + domain="parks", + help_text="Type/category of the park", + max_length=30, + ), + ), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ("operating_season", models.CharField(blank=True, max_length=255)), + ( + "size_acres", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ("website", models.URLField(blank=True)), + ( + "average_rating", + models.DecimalField( + blank=True, decimal_places=2, max_digits=3, null=True + ), + ), + ("ride_count", models.IntegerField(blank=True, null=True)), + ("coaster_count", models.IntegerField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "url", + models.URLField(blank=True, help_text="Frontend URL for this park"), + ), + ( + "opening_year", + models.IntegerField( + blank=True, + db_index=True, + help_text="Year the park opened (computed from opening_date)", + null=True, + ), + ), + ( + "search_text", + models.TextField( + blank=True, + db_index=True, + help_text="Searchable text combining name, description, location, and operator", + ), + ), + ( + "timezone", + models.CharField( + help_text="Timezone identifier for park operations (e.g., 'America/New_York')", + max_length=50, + ), + ), + ], + options={ + "ordering": ["name"], + }, + ), + migrations.CreateModel( + name="ParkArea", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255)), + ("description", models.TextField(blank=True)), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name="ParkAreaEvent", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("description", models.TextField(blank=True)), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ParkEvent", + 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()), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("description", models.TextField(blank=True)), + ( + "status", + apps.core.choices.fields.RichChoiceField( + 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, + ), + ), + ( + "park_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="types", + choices=[ + ("THEME_PARK", "Theme Park"), + ("AMUSEMENT_PARK", "Amusement Park"), + ("WATER_PARK", "Water Park"), + ( + "FAMILY_ENTERTAINMENT_CENTER", + "Family Entertainment Center", + ), + ("CARNIVAL", "Carnival"), + ("FAIR", "Fair"), + ("PIER", "Pier"), + ("BOARDWALK", "Boardwalk"), + ("SAFARI_PARK", "Safari Park"), + ("ZOO", "Zoo"), + ("OTHER", "Other"), + ], + default="THEME_PARK", + domain="parks", + help_text="Type/category of the park", + max_length=30, + ), + ), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ("operating_season", models.CharField(blank=True, max_length=255)), + ( + "size_acres", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ("website", models.URLField(blank=True)), + ( + "average_rating", + models.DecimalField( + blank=True, decimal_places=2, max_digits=3, null=True + ), + ), + ("ride_count", models.IntegerField(blank=True, null=True)), + ("coaster_count", models.IntegerField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "url", + models.URLField(blank=True, help_text="Frontend URL for this park"), + ), + ( + "opening_year", + models.IntegerField( + blank=True, + help_text="Year the park opened (computed from opening_date)", + null=True, + ), + ), + ( + "search_text", + models.TextField( + blank=True, + help_text="Searchable text combining name, description, location, and operator", + ), + ), + ( + "timezone", + models.CharField( + help_text="Timezone identifier for park operations (e.g., 'America/New_York')", + max_length=50, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ParkLocation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("point", models.CharField(blank=True, max_length=50, null=True)), + ("street_address", models.CharField(blank=True, max_length=255)), + ("city", models.CharField(db_index=True, max_length=100)), + ("state", models.CharField(db_index=True, max_length=100)), + ("country", models.CharField(default="USA", max_length=100)), + ( + "continent", + models.CharField( + blank=True, + db_index=True, + help_text="Continent where the park is located", + max_length=50, + ), + ), + ("postal_code", models.CharField(blank=True, max_length=20)), + ("highway_exit", models.CharField(blank=True, max_length=100)), + ("parking_notes", models.TextField(blank=True)), + ("best_arrival_time", models.TimeField(blank=True, null=True)), + ("seasonal_notes", models.TextField(blank=True)), + ("osm_id", models.BigIntegerField(blank=True, null=True)), + ( + "osm_type", + models.CharField( + blank=True, + help_text="Type of OpenStreetMap object (node, way, or relation)", + max_length=10, + ), + ), + ], + options={ + "verbose_name": "Park Location", + "verbose_name_plural": "Park Locations", + "ordering": ["park__name"], + }, + ), + migrations.CreateModel( + name="ParkLocationEvent", + 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()), + ("point", models.CharField(blank=True, max_length=50, null=True)), + ("street_address", models.CharField(blank=True, max_length=255)), + ("city", models.CharField(max_length=100)), + ("state", models.CharField(max_length=100)), + ("country", models.CharField(default="USA", max_length=100)), + ( + "continent", + models.CharField( + blank=True, + help_text="Continent where the park is located", + max_length=50, + ), + ), + ("postal_code", models.CharField(blank=True, max_length=20)), + ("highway_exit", models.CharField(blank=True, max_length=100)), + ("parking_notes", models.TextField(blank=True)), + ("best_arrival_time", models.TimeField(blank=True, null=True)), + ("seasonal_notes", models.TextField(blank=True)), + ("osm_id", models.BigIntegerField(blank=True, null=True)), + ( + "osm_type", + models.CharField( + blank=True, + help_text="Type of OpenStreetMap object (node, way, or relation)", + max_length=10, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ParkPhoto", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("caption", models.CharField(blank=True, max_length=255)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ("is_primary", models.BooleanField(default=False)), + ("is_approved", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date_taken", models.DateTimeField(blank=True, null=True)), + ], + options={ + "ordering": ["-is_primary", "-created_at"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="ParkPhotoEvent", + 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()), + ("caption", models.CharField(blank=True, max_length=255)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ("is_primary", models.BooleanField(default=False)), + ("is_approved", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date_taken", models.DateTimeField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ParkReview", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rating", + models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(10), + ] + ), + ), + ("title", models.CharField(max_length=200)), + ("content", models.TextField()), + ("visit_date", models.DateField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("is_published", models.BooleanField(default=True)), + ("moderation_notes", models.TextField(blank=True)), + ("moderated_at", models.DateTimeField(blank=True, null=True)), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="ParkReviewEvent", + 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()), + ( + "rating", + models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(10), + ] + ), + ), + ("title", models.CharField(max_length=200)), + ("content", models.TextField()), + ("visit_date", models.DateField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("is_published", models.BooleanField(default=True)), + ("moderation_notes", models.TextField(blank=True)), + ("moderated_at", models.DateTimeField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + pgtrigger.migrations.AddTrigger( + model_name="company", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_companyevent" ("created_at", "description", "founded_year", "id", "name", "parks_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_year", NEW."id", NEW."name", NEW."parks_count", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="0ed33eeca3344c43d8124d1f12e3acd3e6fdef02", + 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" ("created_at", "description", "founded_year", "id", "name", "parks_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_year", NEW."id", NEW."name", NEW."parks_count", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="ce9d8347090a033d0a9550419b80a1c4a339216c", + operation="UPDATE", + pgid="pgtrigger_update_update_d3286", + table="parks_company", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="companyevent", + 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="companyevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.company", + ), + ), + migrations.AddField( + model_name="companyheadquarters", + name="company", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="headquarters", + to="parks.company", + ), + ), + migrations.AddField( + model_name="companyheadquartersevent", + name="company", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.company", + ), + ), + migrations.AddField( + model_name="companyheadquartersevent", + 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="companyheadquartersevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.companyheadquarters", + ), + ), + migrations.AddField( + model_name="park", + name="operator", + field=models.ForeignKey( + help_text="Company that operates this park", + limit_choices_to={"roles__contains": ["OPERATOR"]}, + on_delete=django.db.models.deletion.PROTECT, + related_name="operated_parks", + to="parks.company", + ), + ), + migrations.AddField( + model_name="park", + name="property_owner", + field=models.ForeignKey( + blank=True, + help_text="Company that owns the property (if different from operator)", + limit_choices_to={"roles__contains": ["PROPERTY_OWNER"]}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="owned_parks", + to="parks.company", + ), + ), + migrations.AddField( + model_name="parkarea", + name="park", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="areas", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkareaevent", + name="park", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkareaevent", + 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="parkareaevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.parkarea", + ), + ), + migrations.AddField( + model_name="parkevent", + name="operator", + field=models.ForeignKey( + db_constraint=False, + help_text="Company that operates this park", + limit_choices_to={"roles__contains": ["OPERATOR"]}, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.company", + ), + ), + migrations.AddField( + model_name="parkevent", + 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="parkevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkevent", + name="property_owner", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="Company that owns the property (if different from operator)", + limit_choices_to={"roles__contains": ["PROPERTY_OWNER"]}, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.company", + ), + ), + migrations.AddField( + model_name="parklocation", + name="park", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="location", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parklocationevent", + name="park", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parklocationevent", + 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="parklocationevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.parklocation", + ), + ), + migrations.AddField( + model_name="parkphoto", + name="image", + field=models.ForeignKey( + help_text="Park photo stored on Cloudflare Images", + on_delete=django.db.models.deletion.CASCADE, + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + migrations.AddField( + model_name="parkphoto", + name="park", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="photos", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkphoto", + name="uploaded_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="uploaded_park_photos", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="parkevent", + name="banner_image", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="Photo to use as banner image for this park", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.parkphoto", + ), + ), + migrations.AddField( + model_name="parkevent", + name="card_image", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="Photo to use as card image for this park", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.parkphoto", + ), + ), + migrations.AddField( + model_name="park", + name="banner_image", + field=models.ForeignKey( + blank=True, + help_text="Photo to use as banner image for this park", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="parks_using_as_banner", + to="parks.parkphoto", + ), + ), + migrations.AddField( + model_name="park", + name="card_image", + field=models.ForeignKey( + blank=True, + help_text="Photo to use as card image for this park", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="parks_using_as_card", + to="parks.parkphoto", + ), + ), + migrations.AddField( + model_name="parkphotoevent", + name="image", + field=models.ForeignKey( + db_constraint=False, + help_text="Park photo stored on Cloudflare Images", + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + migrations.AddField( + model_name="parkphotoevent", + name="park", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkphotoevent", + 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="parkphotoevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.parkphoto", + ), + ), + migrations.AddField( + model_name="parkphotoevent", + name="uploaded_by", + field=models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="parkreview", + name="moderated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="moderated_park_reviews", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="parkreview", + name="park", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reviews", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkreview", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="park_reviews", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="parkreviewevent", + name="moderated_by", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="parkreviewevent", + name="park", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkreviewevent", + 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="parkreviewevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.parkreview", + ), + ), + migrations.AddField( + model_name="parkreviewevent", + name="user", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddIndex( + model_name="companyheadquarters", + index=models.Index( + fields=["city", "country"], name="parks_compa_city_cf9a4e_idx" + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="companyheadquarters", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_companyheadquartersevent" ("city", "company_id", "country", "created_at", "id", "mailing_address", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "postal_code", "state_province", "street_address", "updated_at") VALUES (NEW."city", NEW."company_id", NEW."country", NEW."created_at", NEW."id", NEW."mailing_address", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."postal_code", NEW."state_province", NEW."street_address", NEW."updated_at"); RETURN NULL;', + hash="acf99673091ec3717f404fdccefd6e0cb228c82e", + operation="INSERT", + pgid="pgtrigger_insert_insert_72259", + table="parks_companyheadquarters", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="companyheadquarters", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "parks_companyheadquartersevent" ("city", "company_id", "country", "created_at", "id", "mailing_address", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "postal_code", "state_province", "street_address", "updated_at") VALUES (NEW."city", NEW."company_id", NEW."country", NEW."created_at", NEW."id", NEW."mailing_address", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."postal_code", NEW."state_province", NEW."street_address", NEW."updated_at"); RETURN NULL;', + hash="bbbff3a1c9748d3ce1b2bf1b705d03ea40530c9b", + operation="UPDATE", + pgid="pgtrigger_update_update_c5392", + table="parks_companyheadquarters", + when="AFTER", + ), + ), + ), + migrations.AlterUniqueTogether( + name="parkarea", + unique_together={("park", "slug")}, + ), + pgtrigger.migrations.AddTrigger( + model_name="parkarea", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;', + hash="fa64ee07f872bf2214b2c1b638b028429752bac4", + operation="INSERT", + pgid="pgtrigger_insert_insert_13457", + table="parks_parkarea", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parkarea", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;', + hash="59fa84527a4fd0fa51685058b6037fa22163a095", + operation="UPDATE", + pgid="pgtrigger_update_update_6e5aa", + table="parks_parkarea", + when="AFTER", + ), + ), + ), + migrations.AddIndex( + model_name="parklocation", + index=models.Index( + fields=["city", "state"], name="parks_parkl_city_7cc873_idx" + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parklocation", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parklocationevent" ("best_arrival_time", "city", "continent", "country", "highway_exit", "id", "osm_id", "osm_type", "park_id", "parking_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "seasonal_notes", "state", "street_address") VALUES (NEW."best_arrival_time", NEW."city", NEW."continent", NEW."country", NEW."highway_exit", NEW."id", NEW."osm_id", NEW."osm_type", NEW."park_id", NEW."parking_notes", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."point", NEW."postal_code", NEW."seasonal_notes", NEW."state", NEW."street_address"); RETURN NULL;', + hash="aecd083c917cea3170e944c73c4906a78eccd676", + operation="INSERT", + pgid="pgtrigger_insert_insert_f8c53", + table="parks_parklocation", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parklocation", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "parks_parklocationevent" ("best_arrival_time", "city", "continent", "country", "highway_exit", "id", "osm_id", "osm_type", "park_id", "parking_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "seasonal_notes", "state", "street_address") VALUES (NEW."best_arrival_time", NEW."city", NEW."continent", NEW."country", NEW."highway_exit", NEW."id", NEW."osm_id", NEW."osm_type", NEW."park_id", NEW."parking_notes", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."point", NEW."postal_code", NEW."seasonal_notes", NEW."state", NEW."street_address"); RETURN NULL;', + hash="a70bc26b34235fe4342009d491d80b990ee3ed7e", + operation="UPDATE", + pgid="pgtrigger_update_update_6dd0d", + table="parks_parklocation", + when="AFTER", + ), + ), + ), + migrations.AddIndex( + model_name="parkphoto", + index=models.Index( + fields=["park", "is_primary"], name="parks_parkp_park_id_eda26e_idx" + ), + ), + migrations.AddIndex( + model_name="parkphoto", + index=models.Index( + fields=["park", "is_approved"], name="parks_parkp_park_id_5fe576_idx" + ), + ), + migrations.AddIndex( + model_name="parkphoto", + index=models.Index( + fields=["created_at"], name="parks_parkp_created_033dc3_idx" + ), + ), + migrations.AddConstraint( + model_name="parkphoto", + constraint=models.UniqueConstraint( + condition=models.Q(("is_primary", True)), + fields=("park",), + name="unique_primary_park_photo", + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parkphoto", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image_id", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image_id", NEW."is_approved", NEW."is_primary", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', + hash="403652164d3e615dae5a14052a56db2851c5cf05", + operation="INSERT", + pgid="pgtrigger_insert_insert_e2033", + table="parks_parkphoto", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parkphoto", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "parks_parkphotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image_id", "is_approved", "is_primary", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image_id", NEW."is_approved", NEW."is_primary", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', + hash="29c60ad09c570b8c03ad6c17052a8f9874314895", + operation="UPDATE", + pgid="pgtrigger_update_update_42711", + table="parks_parkphoto", + when="AFTER", + ), + ), + ), + migrations.AddConstraint( + model_name="park", + constraint=models.CheckConstraint( + condition=models.Q( + ("closing_date__isnull", True), + ("opening_date__isnull", True), + ("closing_date__gte", models.F("opening_date")), + _connector="OR", + ), + name="park_closing_after_opening", + violation_error_message="Closing date must be after opening date", + ), + ), + migrations.AddConstraint( + model_name="park", + constraint=models.CheckConstraint( + condition=models.Q( + ("size_acres__isnull", True), ("size_acres__gt", 0), _connector="OR" + ), + name="park_size_positive", + violation_error_message="Park size must be positive", + ), + ), + migrations.AddConstraint( + model_name="park", + constraint=models.CheckConstraint( + condition=models.Q( + ("average_rating__isnull", True), + models.Q(("average_rating__gte", 1), ("average_rating__lte", 10)), + _connector="OR", + ), + name="park_rating_range", + violation_error_message="Average rating must be between 1 and 10", + ), + ), + migrations.AddConstraint( + model_name="park", + constraint=models.CheckConstraint( + condition=models.Q( + ("ride_count__isnull", True), + ("ride_count__gte", 0), + _connector="OR", + ), + name="park_ride_count_non_negative", + violation_error_message="Ride count must be non-negative", + ), + ), + migrations.AddConstraint( + model_name="park", + constraint=models.CheckConstraint( + condition=models.Q( + ("coaster_count__isnull", True), + ("coaster_count__gte", 0), + _connector="OR", + ), + name="park_coaster_count_non_negative", + violation_error_message="Coaster count must be non-negative", + ), + ), + migrations.AddConstraint( + model_name="park", + constraint=models.CheckConstraint( + condition=models.Q( + ("coaster_count__isnull", True), + ("ride_count__isnull", True), + ("coaster_count__lte", models.F("ride_count")), + _connector="OR", + ), + name="park_coaster_count_lte_ride_count", + violation_error_message="Coaster count cannot exceed total ride count", + ), + ), + 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", "coaster_count", "created_at", "description", "id", "name", "opening_date", "opening_year", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "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."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."opening_year", NEW."operating_season", NEW."operator_id", NEW."park_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", 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="9da686bd8a1881fe7a3fdfebc14411680fe47527", + 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", "coaster_count", "created_at", "description", "id", "name", "opening_date", "opening_year", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "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."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."opening_year", NEW."operating_season", NEW."operator_id", NEW."park_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", 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="787e3176b96b506020f056ee1122d90d25e4cb0d", + operation="UPDATE", + pgid="pgtrigger_update_update_19f56", + table="parks_park", + when="AFTER", + ), + ), + ), + migrations.AddConstraint( + model_name="parkreview", + constraint=models.CheckConstraint( + condition=models.Q(("rating__gte", 1), ("rating__lte", 10)), + name="park_review_rating_range", + violation_error_message="Rating must be between 1 and 10", + ), + ), + migrations.AddConstraint( + model_name="parkreview", + constraint=models.CheckConstraint( + condition=models.Q( + ("visit_date__lte", django.db.models.functions.datetime.Now()) + ), + name="park_review_visit_date_not_future", + violation_error_message="Visit date cannot be in the future", + ), + ), + migrations.AddConstraint( + model_name="parkreview", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q( + ("moderated_at__isnull", True), ("moderated_by__isnull", True) + ), + models.Q( + ("moderated_at__isnull", False), ("moderated_by__isnull", False) + ), + _connector="OR", + ), + name="park_review_moderation_consistency", + violation_error_message="Moderated reviews must have both moderator and moderation timestamp", + ), + ), + migrations.AlterUniqueTogether( + name="parkreview", + unique_together={("park", "user")}, + ), + pgtrigger.migrations.AddTrigger( + model_name="parkreview", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parkreviewevent" ("content", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rating", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;', + hash="fb501d2b3a0d903a03f1a1ff0ae8dd79b189791f", + operation="INSERT", + pgid="pgtrigger_insert_insert_a99bc", + table="parks_parkreview", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parkreview", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "parks_parkreviewevent" ("content", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rating", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;', + hash="254ab0f9ccc0488ea313f1c50a2c35603f7ef02d", + operation="UPDATE", + pgid="pgtrigger_update_update_0e40d", + table="parks_parkreview", + when="AFTER", + ), + ), + ), + ] diff --git a/backend/apps/parks/migrations/__init__.py b/backend/apps/parks/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/rides/migrations/0001_initial.py b/backend/apps/rides/migrations/0001_initial.py new file mode 100644 index 00000000..73031625 --- /dev/null +++ b/backend/apps/rides/migrations/0001_initial.py @@ -0,0 +1,3390 @@ +# Generated by Django 5.2.6 on 2025-09-21 01:27 + +import apps.core.choices.fields +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +import django.db.models.functions.datetime +import django.utils.timezone +import pgtrigger.compiler +import pgtrigger.migrations +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("django_cloudflareimages_toolkit", "__first__"), + ("parks", "0001_initial"), + ("pghistory", "0007_auto_20250421_0444"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Company", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "roles", + django.contrib.postgres.fields.ArrayField( + base_field=apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="company_roles", + choices=[ + ("MANUFACTURER", "Ride Manufacturer"), + ("DESIGNER", "Ride Designer"), + ], + domain="rides", + max_length=20, + ), + blank=True, + default=list, + size=None, + ), + ), + ("description", models.TextField(blank=True)), + ("website", models.URLField(blank=True)), + ("founded_date", models.DateField(blank=True, null=True)), + ("rides_count", models.IntegerField(default=0)), + ("coasters_count", models.IntegerField(default=0)), + ( + "url", + models.URLField( + blank=True, help_text="Frontend URL for this company" + ), + ), + ], + options={ + "verbose_name_plural": "Companies", + "ordering": ["name"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="CompanyEvent", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ( + "roles", + django.contrib.postgres.fields.ArrayField( + base_field=apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="company_roles", + choices=[ + ("MANUFACTURER", "Ride Manufacturer"), + ("DESIGNER", "Ride Designer"), + ], + domain="rides", + max_length=20, + ), + blank=True, + default=list, + size=None, + ), + ), + ("description", models.TextField(blank=True)), + ("website", models.URLField(blank=True)), + ("founded_date", models.DateField(blank=True, null=True)), + ("rides_count", models.IntegerField(default=0)), + ("coasters_count", models.IntegerField(default=0)), + ( + "url", + models.URLField( + blank=True, help_text="Frontend URL for this company" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RankingSnapshot", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("rank", models.PositiveIntegerField()), + ( + "winning_percentage", + models.DecimalField(decimal_places=4, max_digits=5), + ), + ( + "snapshot_date", + models.DateField( + db_index=True, + help_text="Date when this ranking snapshot was taken", + ), + ), + ], + options={ + "ordering": ["-snapshot_date", "rank"], + }, + ), + migrations.CreateModel( + name="Ride", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255)), + ("description", models.TextField(blank=True)), + ( + "category", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="categories", + choices=[ + ("RC", "Roller Coaster"), + ("DR", "Dark Ride"), + ("FR", "Flat Ride"), + ("WR", "Water Ride"), + ("TR", "Transport Ride"), + ("OT", "Other"), + ], + default="", + domain="rides", + help_text="Ride category classification", + max_length=2, + ), + ), + ( + "status", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="statuses", + choices=[ + ("OPERATING", "Operating"), + ("CLOSED_TEMP", "Temporarily Closed"), + ("SBNO", "Standing But Not Operating"), + ("CLOSING", "Closing"), + ("CLOSED_PERM", "Permanently Closed"), + ("UNDER_CONSTRUCTION", "Under Construction"), + ("DEMOLISHED", "Demolished"), + ("RELOCATED", "Relocated"), + ], + default="OPERATING", + domain="rides", + help_text="Current operational status of the ride", + max_length=20, + ), + ), + ( + "post_closing_status", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="post_closing_statuses", + choices=[ + ("SBNO", "Standing But Not Operating"), + ("CLOSED_PERM", "Permanently Closed"), + ], + domain="rides", + help_text="Status to change to after closing date", + max_length=20, + null=True, + ), + ), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ("status_since", models.DateField(blank=True, null=True)), + ("min_height_in", models.PositiveIntegerField(blank=True, null=True)), + ("max_height_in", models.PositiveIntegerField(blank=True, null=True)), + ( + "capacity_per_hour", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "ride_duration_seconds", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "average_rating", + models.DecimalField( + blank=True, decimal_places=2, max_digits=3, null=True + ), + ), + ( + "opening_year", + models.IntegerField(blank=True, db_index=True, null=True), + ), + ("search_text", models.TextField(blank=True, db_index=True)), + ( + "url", + models.URLField(blank=True, help_text="Frontend URL for this ride"), + ), + ( + "park_url", + models.URLField( + blank=True, help_text="Frontend URL for this ride's park" + ), + ), + ], + options={ + "ordering": ["name"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideEvent", + 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)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("description", models.TextField(blank=True)), + ( + "category", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="categories", + choices=[ + ("RC", "Roller Coaster"), + ("DR", "Dark Ride"), + ("FR", "Flat Ride"), + ("WR", "Water Ride"), + ("TR", "Transport Ride"), + ("OT", "Other"), + ], + default="", + domain="rides", + help_text="Ride category classification", + max_length=2, + ), + ), + ( + "status", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="statuses", + choices=[ + ("OPERATING", "Operating"), + ("CLOSED_TEMP", "Temporarily Closed"), + ("SBNO", "Standing But Not Operating"), + ("CLOSING", "Closing"), + ("CLOSED_PERM", "Permanently Closed"), + ("UNDER_CONSTRUCTION", "Under Construction"), + ("DEMOLISHED", "Demolished"), + ("RELOCATED", "Relocated"), + ], + default="OPERATING", + domain="rides", + help_text="Current operational status of the ride", + max_length=20, + ), + ), + ( + "post_closing_status", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="post_closing_statuses", + choices=[ + ("SBNO", "Standing But Not Operating"), + ("CLOSED_PERM", "Permanently Closed"), + ], + domain="rides", + help_text="Status to change to after closing date", + max_length=20, + null=True, + ), + ), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ("status_since", models.DateField(blank=True, null=True)), + ("min_height_in", models.PositiveIntegerField(blank=True, null=True)), + ("max_height_in", models.PositiveIntegerField(blank=True, null=True)), + ( + "capacity_per_hour", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "ride_duration_seconds", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "average_rating", + models.DecimalField( + blank=True, decimal_places=2, max_digits=3, null=True + ), + ), + ("opening_year", models.IntegerField(blank=True, null=True)), + ("search_text", models.TextField(blank=True)), + ( + "url", + models.URLField(blank=True, help_text="Frontend URL for this ride"), + ), + ( + "park_url", + models.URLField( + blank=True, help_text="Frontend URL for this ride's park" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideLocation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("point", models.CharField(blank=True, max_length=50, null=True)), + ( + "park_area", + models.CharField( + blank=True, + db_index=True, + help_text="Themed area or land within the park (e.g., 'Frontierland', 'Tomorrowland')", + max_length=100, + ), + ), + ( + "notes", + models.TextField(blank=True, help_text="General location notes"), + ), + ( + "entrance_notes", + models.TextField( + blank=True, + help_text="Directions to ride entrance, queue location, or navigation tips", + ), + ), + ( + "accessibility_notes", + models.TextField( + blank=True, + help_text="Information about accessible entrances, wheelchair access, etc.", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Ride Location", + "verbose_name_plural": "Ride Locations", + "ordering": ["ride__name"], + }, + ), + migrations.CreateModel( + name="RideLocationEvent", + 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()), + ("point", models.CharField(blank=True, max_length=50, null=True)), + ( + "park_area", + models.CharField( + blank=True, + help_text="Themed area or land within the park (e.g., 'Frontierland', 'Tomorrowland')", + max_length=100, + ), + ), + ( + "notes", + models.TextField(blank=True, help_text="General location notes"), + ), + ( + "entrance_notes", + models.TextField( + blank=True, + help_text="Directions to ride entrance, queue location, or navigation tips", + ), + ), + ( + "accessibility_notes", + models.TextField( + blank=True, + help_text="Information about accessible entrances, wheelchair access, etc.", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModel", + 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)), + ( + "name", + models.CharField( + help_text="Name of the ride model", max_length=255 + ), + ), + ( + "slug", + models.SlugField( + help_text="URL-friendly identifier (unique within manufacturer)", + max_length=255, + ), + ), + ( + "description", + models.TextField( + blank=True, help_text="Detailed description of the ride model" + ), + ), + ( + "category", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="categories", + choices=[ + ("RC", "Roller Coaster"), + ("DR", "Dark Ride"), + ("FR", "Flat Ride"), + ("WR", "Water Ride"), + ("TR", "Transport Ride"), + ("OT", "Other"), + ], + default="", + domain="rides", + help_text="Primary category classification", + max_length=2, + ), + ), + ( + "typical_height_range_min_ft", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Minimum typical height in feet for this model", + max_digits=6, + null=True, + ), + ), + ( + "typical_height_range_max_ft", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Maximum typical height in feet for this model", + max_digits=6, + null=True, + ), + ), + ( + "typical_speed_range_min_mph", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Minimum typical speed in mph for this model", + max_digits=5, + null=True, + ), + ), + ( + "typical_speed_range_max_mph", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Maximum typical speed in mph for this model", + max_digits=5, + null=True, + ), + ), + ( + "typical_capacity_range_min", + models.PositiveIntegerField( + blank=True, + help_text="Minimum typical hourly capacity for this model", + null=True, + ), + ), + ( + "typical_capacity_range_max", + models.PositiveIntegerField( + blank=True, + help_text="Maximum typical hourly capacity for this model", + null=True, + ), + ), + ( + "track_type", + models.CharField( + blank=True, + help_text="Type of track system (e.g., tubular steel, I-Box, wooden)", + max_length=100, + ), + ), + ( + "support_structure", + models.CharField( + blank=True, + help_text="Type of support structure (e.g., steel, wooden, hybrid)", + max_length=100, + ), + ), + ( + "train_configuration", + models.CharField( + blank=True, + help_text="Typical train configuration (e.g., 2 trains, 7 cars per train, 4 seats per car)", + max_length=200, + ), + ), + ( + "restraint_system", + models.CharField( + blank=True, + help_text="Type of restraint system (e.g., over-shoulder, lap bar, vest)", + max_length=100, + ), + ), + ( + "first_installation_year", + models.PositiveIntegerField( + blank=True, + help_text="Year of first installation of this model", + null=True, + ), + ), + ( + "last_installation_year", + models.PositiveIntegerField( + blank=True, + help_text="Year of last installation of this model (if discontinued)", + null=True, + ), + ), + ( + "is_discontinued", + models.BooleanField( + default=False, + help_text="Whether this model is no longer being manufactured", + ), + ), + ( + "total_installations", + models.PositiveIntegerField( + default=0, + help_text="Total number of installations worldwide (auto-calculated)", + ), + ), + ( + "notable_features", + models.TextField( + blank=True, + help_text="Notable design features or innovations (JSON or comma-separated)", + ), + ), + ( + "target_market", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="target_markets", + choices=[ + ("FAMILY", "Family"), + ("THRILL", "Thrill"), + ("EXTREME", "Extreme"), + ("KIDDIE", "Kiddie"), + ("ALL_AGES", "All Ages"), + ], + domain="rides", + help_text="Primary target market for this ride model", + max_length=50, + ), + ), + ( + "meta_title", + models.CharField( + blank=True, + help_text="SEO meta title (auto-generated if blank)", + max_length=60, + ), + ), + ( + "meta_description", + models.CharField( + blank=True, + help_text="SEO meta description (auto-generated if blank)", + max_length=160, + ), + ), + ( + "url", + models.URLField( + blank=True, help_text="Frontend URL for this ride model" + ), + ), + ], + options={ + "ordering": ["manufacturer__name", "name"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelEvent", + 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)), + ( + "name", + models.CharField( + help_text="Name of the ride model", max_length=255 + ), + ), + ( + "slug", + models.SlugField( + db_index=False, + help_text="URL-friendly identifier (unique within manufacturer)", + max_length=255, + ), + ), + ( + "description", + models.TextField( + blank=True, help_text="Detailed description of the ride model" + ), + ), + ( + "category", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="categories", + choices=[ + ("RC", "Roller Coaster"), + ("DR", "Dark Ride"), + ("FR", "Flat Ride"), + ("WR", "Water Ride"), + ("TR", "Transport Ride"), + ("OT", "Other"), + ], + default="", + domain="rides", + help_text="Primary category classification", + max_length=2, + ), + ), + ( + "typical_height_range_min_ft", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Minimum typical height in feet for this model", + max_digits=6, + null=True, + ), + ), + ( + "typical_height_range_max_ft", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Maximum typical height in feet for this model", + max_digits=6, + null=True, + ), + ), + ( + "typical_speed_range_min_mph", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Minimum typical speed in mph for this model", + max_digits=5, + null=True, + ), + ), + ( + "typical_speed_range_max_mph", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Maximum typical speed in mph for this model", + max_digits=5, + null=True, + ), + ), + ( + "typical_capacity_range_min", + models.PositiveIntegerField( + blank=True, + help_text="Minimum typical hourly capacity for this model", + null=True, + ), + ), + ( + "typical_capacity_range_max", + models.PositiveIntegerField( + blank=True, + help_text="Maximum typical hourly capacity for this model", + null=True, + ), + ), + ( + "track_type", + models.CharField( + blank=True, + help_text="Type of track system (e.g., tubular steel, I-Box, wooden)", + max_length=100, + ), + ), + ( + "support_structure", + models.CharField( + blank=True, + help_text="Type of support structure (e.g., steel, wooden, hybrid)", + max_length=100, + ), + ), + ( + "train_configuration", + models.CharField( + blank=True, + help_text="Typical train configuration (e.g., 2 trains, 7 cars per train, 4 seats per car)", + max_length=200, + ), + ), + ( + "restraint_system", + models.CharField( + blank=True, + help_text="Type of restraint system (e.g., over-shoulder, lap bar, vest)", + max_length=100, + ), + ), + ( + "first_installation_year", + models.PositiveIntegerField( + blank=True, + help_text="Year of first installation of this model", + null=True, + ), + ), + ( + "last_installation_year", + models.PositiveIntegerField( + blank=True, + help_text="Year of last installation of this model (if discontinued)", + null=True, + ), + ), + ( + "is_discontinued", + models.BooleanField( + default=False, + help_text="Whether this model is no longer being manufactured", + ), + ), + ( + "total_installations", + models.PositiveIntegerField( + default=0, + help_text="Total number of installations worldwide (auto-calculated)", + ), + ), + ( + "notable_features", + models.TextField( + blank=True, + help_text="Notable design features or innovations (JSON or comma-separated)", + ), + ), + ( + "target_market", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="target_markets", + choices=[ + ("FAMILY", "Family"), + ("THRILL", "Thrill"), + ("EXTREME", "Extreme"), + ("KIDDIE", "Kiddie"), + ("ALL_AGES", "All Ages"), + ], + domain="rides", + help_text="Primary target market for this ride model", + max_length=50, + ), + ), + ( + "meta_title", + models.CharField( + blank=True, + help_text="SEO meta title (auto-generated if blank)", + max_length=60, + ), + ), + ( + "meta_description", + models.CharField( + blank=True, + help_text="SEO meta description (auto-generated if blank)", + max_length=160, + ), + ), + ( + "url", + models.URLField( + blank=True, help_text="Frontend URL for this ride model" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelPhoto", + 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)), + ("caption", models.CharField(blank=True, max_length=500)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ( + "photo_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="photo_types", + choices=[ + ("PROMOTIONAL", "Promotional"), + ("TECHNICAL", "Technical Drawing"), + ("INSTALLATION", "Installation Example"), + ("RENDERING", "3D Rendering"), + ("CATALOG", "Catalog Image"), + ], + default="PROMOTIONAL", + domain="rides", + help_text="Type of photo for categorization and display purposes", + max_length=20, + ), + ), + ( + "is_primary", + models.BooleanField( + default=False, + help_text="Whether this is the primary photo for the ride model", + ), + ), + ("photographer", models.CharField(blank=True, max_length=255)), + ("source", models.CharField(blank=True, max_length=255)), + ("copyright_info", models.CharField(blank=True, max_length=255)), + ], + options={ + "ordering": ["-is_primary", "-created_at"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelPhotoEvent", + 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)), + ("caption", models.CharField(blank=True, max_length=500)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ( + "photo_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="photo_types", + choices=[ + ("PROMOTIONAL", "Promotional"), + ("TECHNICAL", "Technical Drawing"), + ("INSTALLATION", "Installation Example"), + ("RENDERING", "3D Rendering"), + ("CATALOG", "Catalog Image"), + ], + default="PROMOTIONAL", + domain="rides", + help_text="Type of photo for categorization and display purposes", + max_length=20, + ), + ), + ( + "is_primary", + models.BooleanField( + default=False, + help_text="Whether this is the primary photo for the ride model", + ), + ), + ("photographer", models.CharField(blank=True, max_length=255)), + ("source", models.CharField(blank=True, max_length=255)), + ("copyright_info", models.CharField(blank=True, max_length=255)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelTechnicalSpec", + 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)), + ( + "spec_category", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="spec_categories", + choices=[ + ("DIMENSIONS", "Dimensions"), + ("PERFORMANCE", "Performance"), + ("CAPACITY", "Capacity"), + ("SAFETY", "Safety Features"), + ("ELECTRICAL", "Electrical Requirements"), + ("FOUNDATION", "Foundation Requirements"), + ("MAINTENANCE", "Maintenance"), + ("OTHER", "Other"), + ], + domain="rides", + help_text="Category of technical specification", + max_length=50, + ), + ), + ( + "spec_name", + models.CharField( + help_text="Name of the specification", max_length=100 + ), + ), + ( + "spec_value", + models.CharField( + help_text="Value of the specification", max_length=255 + ), + ), + ( + "spec_unit", + models.CharField( + blank=True, help_text="Unit of measurement", max_length=20 + ), + ), + ( + "notes", + models.TextField( + blank=True, + help_text="Additional notes about this specification", + ), + ), + ], + options={ + "ordering": ["spec_category", "spec_name"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelTechnicalSpecEvent", + 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)), + ( + "spec_category", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="spec_categories", + choices=[ + ("DIMENSIONS", "Dimensions"), + ("PERFORMANCE", "Performance"), + ("CAPACITY", "Capacity"), + ("SAFETY", "Safety Features"), + ("ELECTRICAL", "Electrical Requirements"), + ("FOUNDATION", "Foundation Requirements"), + ("MAINTENANCE", "Maintenance"), + ("OTHER", "Other"), + ], + domain="rides", + help_text="Category of technical specification", + max_length=50, + ), + ), + ( + "spec_name", + models.CharField( + help_text="Name of the specification", max_length=100 + ), + ), + ( + "spec_value", + models.CharField( + help_text="Value of the specification", max_length=255 + ), + ), + ( + "spec_unit", + models.CharField( + blank=True, help_text="Unit of measurement", max_length=20 + ), + ), + ( + "notes", + models.TextField( + blank=True, + help_text="Additional notes about this specification", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelVariant", + 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)), + ( + "name", + models.CharField(help_text="Name of this variant", max_length=255), + ), + ( + "description", + models.TextField( + blank=True, help_text="Description of variant differences" + ), + ), + ( + "min_height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "max_height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "min_speed_mph", + models.DecimalField( + blank=True, decimal_places=2, max_digits=5, null=True + ), + ), + ( + "max_speed_mph", + models.DecimalField( + blank=True, decimal_places=2, max_digits=5, null=True + ), + ), + ( + "distinguishing_features", + models.TextField( + blank=True, + help_text="What makes this variant unique from the base model", + ), + ), + ], + options={ + "ordering": ["ride_model", "name"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelVariantEvent", + 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)), + ( + "name", + models.CharField(help_text="Name of this variant", max_length=255), + ), + ( + "description", + models.TextField( + blank=True, help_text="Description of variant differences" + ), + ), + ( + "min_height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "max_height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "min_speed_mph", + models.DecimalField( + blank=True, decimal_places=2, max_digits=5, null=True + ), + ), + ( + "max_speed_mph", + models.DecimalField( + blank=True, decimal_places=2, max_digits=5, null=True + ), + ), + ( + "distinguishing_features", + models.TextField( + blank=True, + help_text="What makes this variant unique from the base model", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RidePairComparison", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "ride_a_wins", + models.PositiveIntegerField( + default=0, + help_text="Number of mutual riders who rated ride_a higher", + ), + ), + ( + "ride_b_wins", + models.PositiveIntegerField( + default=0, + help_text="Number of mutual riders who rated ride_b higher", + ), + ), + ( + "ties", + models.PositiveIntegerField( + default=0, + help_text="Number of mutual riders who rated both rides equally", + ), + ), + ( + "mutual_riders_count", + models.PositiveIntegerField( + default=0, + help_text="Total number of users who have rated both rides", + ), + ), + ( + "ride_a_avg_rating", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Average rating of ride_a from mutual riders", + max_digits=3, + null=True, + ), + ), + ( + "ride_b_avg_rating", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Average rating of ride_b from mutual riders", + max_digits=3, + null=True, + ), + ), + ( + "last_calculated", + models.DateTimeField( + auto_now=True, + help_text="When this comparison was last calculated", + ), + ), + ], + ), + migrations.CreateModel( + name="RidePairComparisonEvent", + 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()), + ( + "ride_a_wins", + models.PositiveIntegerField( + default=0, + help_text="Number of mutual riders who rated ride_a higher", + ), + ), + ( + "ride_b_wins", + models.PositiveIntegerField( + default=0, + help_text="Number of mutual riders who rated ride_b higher", + ), + ), + ( + "ties", + models.PositiveIntegerField( + default=0, + help_text="Number of mutual riders who rated both rides equally", + ), + ), + ( + "mutual_riders_count", + models.PositiveIntegerField( + default=0, + help_text="Total number of users who have rated both rides", + ), + ), + ( + "ride_a_avg_rating", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Average rating of ride_a from mutual riders", + max_digits=3, + null=True, + ), + ), + ( + "ride_b_avg_rating", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Average rating of ride_b from mutual riders", + max_digits=3, + null=True, + ), + ), + ( + "last_calculated", + models.DateTimeField( + auto_now=True, + help_text="When this comparison was last calculated", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RidePhoto", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("caption", models.CharField(blank=True, max_length=255)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ("is_primary", models.BooleanField(default=False)), + ("is_approved", models.BooleanField(default=False)), + ( + "photo_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="photo_types", + choices=[ + ("PROMOTIONAL", "Promotional"), + ("TECHNICAL", "Technical Drawing"), + ("INSTALLATION", "Installation Example"), + ("RENDERING", "3D Rendering"), + ("CATALOG", "Catalog Image"), + ], + default="exterior", + domain="rides", + help_text="Type of photo for categorization and display purposes", + max_length=50, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date_taken", models.DateTimeField(blank=True, null=True)), + ], + options={ + "ordering": ["-is_primary", "-created_at"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="RidePhotoEvent", + 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()), + ("caption", models.CharField(blank=True, max_length=255)), + ("alt_text", models.CharField(blank=True, max_length=255)), + ("is_primary", models.BooleanField(default=False)), + ("is_approved", models.BooleanField(default=False)), + ( + "photo_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="photo_types", + choices=[ + ("PROMOTIONAL", "Promotional"), + ("TECHNICAL", "Technical Drawing"), + ("INSTALLATION", "Installation Example"), + ("RENDERING", "3D Rendering"), + ("CATALOG", "Catalog Image"), + ], + default="exterior", + domain="rides", + help_text="Type of photo for categorization and display purposes", + max_length=50, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date_taken", models.DateTimeField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideRanking", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rank", + models.PositiveIntegerField( + db_index=True, help_text="Overall rank position (1 = best)" + ), + ), + ( + "wins", + models.PositiveIntegerField( + default=0, + help_text="Number of rides this ride beats in pairwise comparisons", + ), + ), + ( + "losses", + models.PositiveIntegerField( + default=0, + help_text="Number of rides that beat this ride in pairwise comparisons", + ), + ), + ( + "ties", + models.PositiveIntegerField( + default=0, + help_text="Number of rides with equal preference in pairwise comparisons", + ), + ), + ( + "winning_percentage", + models.DecimalField( + db_index=True, + decimal_places=4, + help_text="Win percentage where ties count as 0.5", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + ), + ), + ( + "mutual_riders_count", + models.PositiveIntegerField( + default=0, + help_text="Total number of users who have rated this ride", + ), + ), + ( + "comparison_count", + models.PositiveIntegerField( + default=0, + help_text="Number of other rides this was compared against", + ), + ), + ( + "average_rating", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Average rating from all users who have rated this ride", + max_digits=3, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(10), + ], + ), + ), + ( + "last_calculated", + models.DateTimeField( + default=django.utils.timezone.now, + help_text="When this ranking was last calculated", + ), + ), + ( + "calculation_version", + models.CharField( + default="1.0", + help_text="Algorithm version used for calculation", + max_length=10, + ), + ), + ], + options={ + "ordering": ["rank"], + }, + ), + migrations.CreateModel( + name="RideRankingEvent", + 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()), + ( + "rank", + models.PositiveIntegerField( + help_text="Overall rank position (1 = best)" + ), + ), + ( + "wins", + models.PositiveIntegerField( + default=0, + help_text="Number of rides this ride beats in pairwise comparisons", + ), + ), + ( + "losses", + models.PositiveIntegerField( + default=0, + help_text="Number of rides that beat this ride in pairwise comparisons", + ), + ), + ( + "ties", + models.PositiveIntegerField( + default=0, + help_text="Number of rides with equal preference in pairwise comparisons", + ), + ), + ( + "winning_percentage", + models.DecimalField( + decimal_places=4, + help_text="Win percentage where ties count as 0.5", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + ), + ), + ( + "mutual_riders_count", + models.PositiveIntegerField( + default=0, + help_text="Total number of users who have rated this ride", + ), + ), + ( + "comparison_count", + models.PositiveIntegerField( + default=0, + help_text="Number of other rides this was compared against", + ), + ), + ( + "average_rating", + models.DecimalField( + blank=True, + decimal_places=2, + help_text="Average rating from all users who have rated this ride", + max_digits=3, + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(10), + ], + ), + ), + ( + "last_calculated", + models.DateTimeField( + default=django.utils.timezone.now, + help_text="When this ranking was last calculated", + ), + ), + ( + "calculation_version", + models.CharField( + default="1.0", + help_text="Algorithm version used for calculation", + max_length=10, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideReview", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rating", + models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(10), + ] + ), + ), + ("title", models.CharField(max_length=200)), + ("content", models.TextField()), + ("visit_date", models.DateField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("is_published", models.BooleanField(default=True)), + ("moderation_notes", models.TextField(blank=True)), + ("moderated_at", models.DateTimeField(blank=True, null=True)), + ], + options={ + "ordering": ["-created_at"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideReviewEvent", + 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()), + ( + "rating", + models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(10), + ] + ), + ), + ("title", models.CharField(max_length=200)), + ("content", models.TextField()), + ("visit_date", models.DateField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("is_published", models.BooleanField(default=True)), + ("moderation_notes", models.TextField(blank=True)), + ("moderated_at", models.DateTimeField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RollerCoasterStats", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "length_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=7, null=True + ), + ), + ( + "speed_mph", + models.DecimalField( + blank=True, decimal_places=2, max_digits=5, null=True + ), + ), + ("inversions", models.PositiveIntegerField(default=0)), + ( + "ride_time_seconds", + models.PositiveIntegerField(blank=True, null=True), + ), + ("track_type", models.CharField(blank=True, max_length=255)), + ( + "track_material", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="track_materials", + choices=[ + ("STEEL", "Steel"), + ("WOOD", "Wood"), + ("HYBRID", "Hybrid"), + ], + default="STEEL", + domain="rides", + help_text="Track construction material type", + max_length=20, + ), + ), + ( + "roller_coaster_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="coaster_types", + choices=[ + ("SITDOWN", "Sit Down"), + ("INVERTED", "Inverted"), + ("FLYING", "Flying"), + ("STANDUP", "Stand Up"), + ("WING", "Wing"), + ("DIVE", "Dive"), + ("FAMILY", "Family"), + ("WILD_MOUSE", "Wild Mouse"), + ("SPINNING", "Spinning"), + ("FOURTH_DIMENSION", "4th Dimension"), + ("OTHER", "Other"), + ], + default="SITDOWN", + domain="rides", + help_text="Roller coaster type classification", + max_length=20, + ), + ), + ( + "max_drop_height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "propulsion_system", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="propulsion_systems", + choices=[ + ("CHAIN", "Chain Lift"), + ("LSM", "LSM Launch"), + ("HYDRAULIC", "Hydraulic Launch"), + ("GRAVITY", "Gravity"), + ("OTHER", "Other"), + ], + default="CHAIN", + domain="rides", + help_text="Propulsion or lift system type", + max_length=20, + ), + ), + ("train_style", models.CharField(blank=True, max_length=255)), + ("trains_count", models.PositiveIntegerField(blank=True, null=True)), + ("cars_per_train", models.PositiveIntegerField(blank=True, null=True)), + ("seats_per_car", models.PositiveIntegerField(blank=True, null=True)), + ], + options={ + "verbose_name": "Roller Coaster Statistics", + "verbose_name_plural": "Roller Coaster Statistics", + }, + ), + migrations.CreateModel( + name="RollerCoasterStatsEvent", + 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()), + ( + "height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "length_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=7, null=True + ), + ), + ( + "speed_mph", + models.DecimalField( + blank=True, decimal_places=2, max_digits=5, null=True + ), + ), + ("inversions", models.PositiveIntegerField(default=0)), + ( + "ride_time_seconds", + models.PositiveIntegerField(blank=True, null=True), + ), + ("track_type", models.CharField(blank=True, max_length=255)), + ( + "track_material", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="track_materials", + choices=[ + ("STEEL", "Steel"), + ("WOOD", "Wood"), + ("HYBRID", "Hybrid"), + ], + default="STEEL", + domain="rides", + help_text="Track construction material type", + max_length=20, + ), + ), + ( + "roller_coaster_type", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + blank=True, + choice_group="coaster_types", + choices=[ + ("SITDOWN", "Sit Down"), + ("INVERTED", "Inverted"), + ("FLYING", "Flying"), + ("STANDUP", "Stand Up"), + ("WING", "Wing"), + ("DIVE", "Dive"), + ("FAMILY", "Family"), + ("WILD_MOUSE", "Wild Mouse"), + ("SPINNING", "Spinning"), + ("FOURTH_DIMENSION", "4th Dimension"), + ("OTHER", "Other"), + ], + default="SITDOWN", + domain="rides", + help_text="Roller coaster type classification", + max_length=20, + ), + ), + ( + "max_drop_height_ft", + models.DecimalField( + blank=True, decimal_places=2, max_digits=6, null=True + ), + ), + ( + "propulsion_system", + apps.core.choices.fields.RichChoiceField( + allow_deprecated=False, + choice_group="propulsion_systems", + choices=[ + ("CHAIN", "Chain Lift"), + ("LSM", "LSM Launch"), + ("HYDRAULIC", "Hydraulic Launch"), + ("GRAVITY", "Gravity"), + ("OTHER", "Other"), + ], + default="CHAIN", + domain="rides", + help_text="Propulsion or lift system type", + max_length=20, + ), + ), + ("train_style", models.CharField(blank=True, max_length=255)), + ("trains_count", models.PositiveIntegerField(blank=True, null=True)), + ("cars_per_train", models.PositiveIntegerField(blank=True, null=True)), + ("seats_per_car", models.PositiveIntegerField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + pgtrigger.migrations.AddTrigger( + model_name="company", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_companyevent" ("coasters_count", "created_at", "description", "founded_date", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "url", "website") VALUES (NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', + hash="fe6c1e3f09822f5e7f716cd83483cf152ec138f0", + operation="INSERT", + pgid="pgtrigger_insert_insert_e7194", + table="rides_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 "rides_companyevent" ("coasters_count", "created_at", "description", "founded_date", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "url", "website") VALUES (NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', + hash="0b76cb36b7551ed3e64e674b8cfe343d4d2ec306", + operation="UPDATE", + pgid="pgtrigger_update_update_456a8", + table="rides_company", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="companyevent", + 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="companyevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.company", + ), + ), + migrations.AddField( + model_name="ride", + name="designer", + field=models.ForeignKey( + blank=True, + limit_choices_to={"roles__contains": ["DESIGNER"]}, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="designed_rides", + to="rides.company", + ), + ), + migrations.AddField( + model_name="ride", + name="manufacturer", + field=models.ForeignKey( + blank=True, + limit_choices_to={"roles__contains": ["MANUFACTURER"]}, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="manufactured_rides", + to="rides.company", + ), + ), + migrations.AddField( + model_name="ride", + name="park", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="rides", + to="parks.park", + ), + ), + migrations.AddField( + model_name="ride", + name="park_area", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="rides", + to="parks.parkarea", + ), + ), + migrations.AddField( + model_name="rankingsnapshot", + name="ride", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ranking_history", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="rideevent", + name="designer", + field=models.ForeignKey( + blank=True, + db_constraint=False, + limit_choices_to={"roles__contains": ["DESIGNER"]}, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.company", + ), + ), + migrations.AddField( + model_name="rideevent", + name="manufacturer", + field=models.ForeignKey( + blank=True, + db_constraint=False, + limit_choices_to={"roles__contains": ["MANUFACTURER"]}, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.company", + ), + ), + migrations.AddField( + model_name="rideevent", + name="park", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.park", + ), + ), + migrations.AddField( + model_name="rideevent", + name="park_area", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.parkarea", + ), + ), + migrations.AddField( + model_name="rideevent", + 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="rideevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridelocation", + name="ride", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="ride_location", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridelocationevent", + 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="ridelocationevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridelocation", + ), + ), + migrations.AddField( + model_name="ridelocationevent", + name="ride", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridemodel", + name="manufacturer", + field=models.ForeignKey( + blank=True, + help_text="Primary manufacturer of this ride model", + limit_choices_to={"roles__contains": ["MANUFACTURER"]}, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="ride_models", + to="rides.company", + ), + ), + migrations.AddField( + model_name="rideevent", + name="ride_model", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="The specific model/type of this ride", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ride", + name="ride_model", + field=models.ForeignKey( + blank=True, + help_text="The specific model/type of this ride", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="rides", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodelevent", + name="manufacturer", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="Primary manufacturer of this ride model", + limit_choices_to={"roles__contains": ["MANUFACTURER"]}, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.company", + ), + ), + migrations.AddField( + model_name="ridemodelevent", + 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="ridemodelevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodelphoto", + name="image", + field=models.ForeignKey( + help_text="Photo of the ride model stored on Cloudflare Images", + on_delete=django.db.models.deletion.CASCADE, + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + migrations.AddField( + model_name="ridemodelphoto", + name="ride_model", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="photos", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodelevent", + name="primary_image", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="Primary promotional image for this ride model", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridemodelphoto", + ), + ), + migrations.AddField( + model_name="ridemodel", + name="primary_image", + field=models.ForeignKey( + blank=True, + help_text="Primary promotional image for this ride model", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="ride_models_as_primary", + to="rides.ridemodelphoto", + ), + ), + migrations.AddField( + model_name="ridemodelphotoevent", + name="image", + field=models.ForeignKey( + db_constraint=False, + help_text="Photo of the ride model stored on Cloudflare Images", + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + migrations.AddField( + model_name="ridemodelphotoevent", + 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="ridemodelphotoevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridemodelphoto", + ), + ), + migrations.AddField( + model_name="ridemodelphotoevent", + name="ride_model", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodeltechnicalspec", + name="ride_model", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="technical_specs", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodeltechnicalspecevent", + 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="ridemodeltechnicalspecevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridemodeltechnicalspec", + ), + ), + migrations.AddField( + model_name="ridemodeltechnicalspecevent", + name="ride_model", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodelvariant", + name="ride_model", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="variants", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodelvariantevent", + 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="ridemodelvariantevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridemodelvariant", + ), + ), + migrations.AddField( + model_name="ridemodelvariantevent", + name="ride_model", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridepaircomparison", + name="ride_a", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comparisons_as_a", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridepaircomparison", + name="ride_b", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comparisons_as_b", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridepaircomparisonevent", + 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="ridepaircomparisonevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridepaircomparison", + ), + ), + migrations.AddField( + model_name="ridepaircomparisonevent", + name="ride_a", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridepaircomparisonevent", + name="ride_b", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridephoto", + name="image", + field=models.ForeignKey( + help_text="Ride photo stored on Cloudflare Images", + on_delete=django.db.models.deletion.CASCADE, + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + migrations.AddField( + model_name="ridephoto", + name="ride", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="photos", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridephoto", + name="uploaded_by", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="uploaded_ride_photos", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="rideevent", + name="banner_image", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="Photo to use as banner image for this ride", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridephoto", + ), + ), + migrations.AddField( + model_name="rideevent", + name="card_image", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="Photo to use as card image for this ride", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridephoto", + ), + ), + migrations.AddField( + model_name="ride", + name="banner_image", + field=models.ForeignKey( + blank=True, + help_text="Photo to use as banner image for this ride", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="rides_using_as_banner", + to="rides.ridephoto", + ), + ), + migrations.AddField( + model_name="ride", + name="card_image", + field=models.ForeignKey( + blank=True, + help_text="Photo to use as card image for this ride", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="rides_using_as_card", + to="rides.ridephoto", + ), + ), + migrations.AddField( + model_name="ridephotoevent", + name="image", + field=models.ForeignKey( + db_constraint=False, + help_text="Ride photo stored on Cloudflare Images", + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="django_cloudflareimages_toolkit.cloudflareimage", + ), + ), + migrations.AddField( + model_name="ridephotoevent", + 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="ridephotoevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridephoto", + ), + ), + migrations.AddField( + model_name="ridephotoevent", + name="ride", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridephotoevent", + name="uploaded_by", + field=models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="rideranking", + name="ride", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="ranking", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="riderankingevent", + 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="riderankingevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.rideranking", + ), + ), + migrations.AddField( + model_name="riderankingevent", + name="ride", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridereview", + name="moderated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="moderated_ride_reviews", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="ridereview", + name="ride", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reviews", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridereview", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ride_reviews", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="ridereviewevent", + name="moderated_by", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="ridereviewevent", + 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="ridereviewevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridereview", + ), + ), + migrations.AddField( + model_name="ridereviewevent", + name="ride", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="ridereviewevent", + name="user", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="rollercoasterstats", + name="ride", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="coaster_stats", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="rollercoasterstatsevent", + 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="rollercoasterstatsevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.rollercoasterstats", + ), + ), + migrations.AddField( + model_name="rollercoasterstatsevent", + name="ride", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ride", + ), + ), + migrations.AddIndex( + model_name="rankingsnapshot", + index=models.Index( + fields=["snapshot_date", "rank"], name="rides_ranki_snapsho_8e2657_idx" + ), + ), + migrations.AddIndex( + model_name="rankingsnapshot", + index=models.Index( + fields=["ride", "-snapshot_date"], name="rides_ranki_ride_id_827bb9_idx" + ), + ), + migrations.AlterUniqueTogether( + name="rankingsnapshot", + unique_together={("ride", "snapshot_date")}, + ), + migrations.AddIndex( + model_name="ridelocation", + index=models.Index( + fields=["park_area"], name="rides_ridel_park_ar_26c90c_idx" + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridelocation", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridelocationevent" ("accessibility_notes", "created_at", "entrance_notes", "id", "notes", "park_area", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "ride_id", "updated_at") VALUES (NEW."accessibility_notes", NEW."created_at", NEW."entrance_notes", NEW."id", NEW."notes", NEW."park_area", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."point", NEW."ride_id", NEW."updated_at"); RETURN NULL;', + hash="04c4c3aa17d4ef852d52b40d1dba4cd7372d5e29", + operation="INSERT", + pgid="pgtrigger_insert_insert_b66c2", + table="rides_ridelocation", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridelocation", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridelocationevent" ("accessibility_notes", "created_at", "entrance_notes", "id", "notes", "park_area", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "ride_id", "updated_at") VALUES (NEW."accessibility_notes", NEW."created_at", NEW."entrance_notes", NEW."id", NEW."notes", NEW."park_area", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."point", NEW."ride_id", NEW."updated_at"); RETURN NULL;', + hash="7073b4517d00b884b2f3fddf89caeefaa64058ad", + operation="UPDATE", + pgid="pgtrigger_update_update_402ba", + table="rides_ridelocation", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodelphoto", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridemodelphotoevent" ("alt_text", "caption", "copyright_info", "created_at", "id", "image_id", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "photographer", "ride_model_id", "source", "updated_at") VALUES (NEW."alt_text", NEW."caption", NEW."copyright_info", NEW."created_at", NEW."id", NEW."image_id", NEW."is_primary", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photo_type", NEW."photographer", NEW."ride_model_id", NEW."source", NEW."updated_at"); RETURN NULL;', + hash="fa289c31e25da0c08740d9e9c4072f3e4df81c42", + operation="INSERT", + pgid="pgtrigger_insert_insert_c5e58", + table="rides_ridemodelphoto", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodelphoto", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridemodelphotoevent" ("alt_text", "caption", "copyright_info", "created_at", "id", "image_id", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "photographer", "ride_model_id", "source", "updated_at") VALUES (NEW."alt_text", NEW."caption", NEW."copyright_info", NEW."created_at", NEW."id", NEW."image_id", NEW."is_primary", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photo_type", NEW."photographer", NEW."ride_model_id", NEW."source", NEW."updated_at"); RETURN NULL;', + hash="1ead1d3fd3dd553f585ae76aa6f3215314322ff4", + operation="UPDATE", + pgid="pgtrigger_update_update_3afcd", + table="rides_ridemodelphoto", + when="AFTER", + ), + ), + ), + migrations.AddConstraint( + model_name="ridemodel", + constraint=models.CheckConstraint( + condition=models.Q( + ("typical_height_range_min_ft__isnull", True), + ("typical_height_range_max_ft__isnull", True), + ( + "typical_height_range_min_ft__lte", + models.F("typical_height_range_max_ft"), + ), + _connector="OR", + ), + name="ride_model_height_range_logical", + violation_error_message="Minimum height cannot exceed maximum height", + ), + ), + migrations.AddConstraint( + model_name="ridemodel", + constraint=models.CheckConstraint( + condition=models.Q( + ("typical_speed_range_min_mph__isnull", True), + ("typical_speed_range_max_mph__isnull", True), + ( + "typical_speed_range_min_mph__lte", + models.F("typical_speed_range_max_mph"), + ), + _connector="OR", + ), + name="ride_model_speed_range_logical", + violation_error_message="Minimum speed cannot exceed maximum speed", + ), + ), + migrations.AddConstraint( + model_name="ridemodel", + constraint=models.CheckConstraint( + condition=models.Q( + ("typical_capacity_range_min__isnull", True), + ("typical_capacity_range_max__isnull", True), + ( + "typical_capacity_range_min__lte", + models.F("typical_capacity_range_max"), + ), + _connector="OR", + ), + name="ride_model_capacity_range_logical", + violation_error_message="Minimum capacity cannot exceed maximum capacity", + ), + ), + migrations.AddConstraint( + model_name="ridemodel", + constraint=models.CheckConstraint( + condition=models.Q( + ("first_installation_year__isnull", True), + ("last_installation_year__isnull", True), + ( + "first_installation_year__lte", + models.F("last_installation_year"), + ), + _connector="OR", + ), + name="ride_model_installation_years_logical", + violation_error_message="First installation year cannot be after last installation year", + ), + ), + migrations.AlterUniqueTogether( + name="ridemodel", + unique_together={("manufacturer", "name"), ("manufacturer", "slug")}, + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodel", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridemodelevent" ("category", "created_at", "description", "first_installation_year", "id", "is_discontinued", "last_installation_year", "manufacturer_id", "meta_description", "meta_title", "name", "notable_features", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "primary_image_id", "restraint_system", "slug", "support_structure", "target_market", "total_installations", "track_type", "train_configuration", "typical_capacity_range_max", "typical_capacity_range_min", "typical_height_range_max_ft", "typical_height_range_min_ft", "typical_speed_range_max_mph", "typical_speed_range_min_mph", "updated_at", "url") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."first_installation_year", NEW."id", NEW."is_discontinued", NEW."last_installation_year", NEW."manufacturer_id", NEW."meta_description", NEW."meta_title", NEW."name", NEW."notable_features", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."primary_image_id", NEW."restraint_system", NEW."slug", NEW."support_structure", NEW."target_market", NEW."total_installations", NEW."track_type", NEW."train_configuration", NEW."typical_capacity_range_max", NEW."typical_capacity_range_min", NEW."typical_height_range_max_ft", NEW."typical_height_range_min_ft", NEW."typical_speed_range_max_mph", NEW."typical_speed_range_min_mph", NEW."updated_at", NEW."url"); RETURN NULL;', + hash="9cee65f580a26ae9edc8f9fc1f3d9b25da1856c3", + operation="INSERT", + pgid="pgtrigger_insert_insert_0aaee", + table="rides_ridemodel", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodel", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridemodelevent" ("category", "created_at", "description", "first_installation_year", "id", "is_discontinued", "last_installation_year", "manufacturer_id", "meta_description", "meta_title", "name", "notable_features", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "primary_image_id", "restraint_system", "slug", "support_structure", "target_market", "total_installations", "track_type", "train_configuration", "typical_capacity_range_max", "typical_capacity_range_min", "typical_height_range_max_ft", "typical_height_range_min_ft", "typical_speed_range_max_mph", "typical_speed_range_min_mph", "updated_at", "url") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."first_installation_year", NEW."id", NEW."is_discontinued", NEW."last_installation_year", NEW."manufacturer_id", NEW."meta_description", NEW."meta_title", NEW."name", NEW."notable_features", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."primary_image_id", NEW."restraint_system", NEW."slug", NEW."support_structure", NEW."target_market", NEW."total_installations", NEW."track_type", NEW."train_configuration", NEW."typical_capacity_range_max", NEW."typical_capacity_range_min", NEW."typical_height_range_max_ft", NEW."typical_height_range_min_ft", NEW."typical_speed_range_max_mph", NEW."typical_speed_range_min_mph", NEW."updated_at", NEW."url"); RETURN NULL;', + hash="365f87607f9f7bfee1caaabdd32b16032e04ae82", + operation="UPDATE", + pgid="pgtrigger_update_update_0ca1a", + table="rides_ridemodel", + when="AFTER", + ), + ), + ), + migrations.AlterUniqueTogether( + name="ridemodeltechnicalspec", + unique_together={("ride_model", "spec_category", "spec_name")}, + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodeltechnicalspec", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridemodeltechnicalspecevent" ("created_at", "id", "notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_model_id", "spec_category", "spec_name", "spec_unit", "spec_value", "updated_at") VALUES (NEW."created_at", NEW."id", NEW."notes", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."ride_model_id", NEW."spec_category", NEW."spec_name", NEW."spec_unit", NEW."spec_value", NEW."updated_at"); RETURN NULL;', + hash="c69e4b67f99f3c6135baa3b1f90005dd1a28fc99", + operation="INSERT", + pgid="pgtrigger_insert_insert_08870", + table="rides_ridemodeltechnicalspec", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodeltechnicalspec", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridemodeltechnicalspecevent" ("created_at", "id", "notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_model_id", "spec_category", "spec_name", "spec_unit", "spec_value", "updated_at") VALUES (NEW."created_at", NEW."id", NEW."notes", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."ride_model_id", NEW."spec_category", NEW."spec_name", NEW."spec_unit", NEW."spec_value", NEW."updated_at"); RETURN NULL;', + hash="9c6cdcf25f220fe155970cde66cc79e98ad44142", + operation="UPDATE", + pgid="pgtrigger_update_update_73620", + table="rides_ridemodeltechnicalspec", + when="AFTER", + ), + ), + ), + migrations.AlterUniqueTogether( + name="ridemodelvariant", + unique_together={("ride_model", "name")}, + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodelvariant", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridemodelvariantevent" ("created_at", "description", "distinguishing_features", "id", "max_height_ft", "max_speed_mph", "min_height_ft", "min_speed_mph", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_model_id", "updated_at") VALUES (NEW."created_at", NEW."description", NEW."distinguishing_features", NEW."id", NEW."max_height_ft", NEW."max_speed_mph", NEW."min_height_ft", NEW."min_speed_mph", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."ride_model_id", NEW."updated_at"); RETURN NULL;', + hash="89d68cdcd08787e00dd8d1f25e9229eb02528f26", + operation="INSERT", + pgid="pgtrigger_insert_insert_1cb69", + table="rides_ridemodelvariant", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridemodelvariant", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridemodelvariantevent" ("created_at", "description", "distinguishing_features", "id", "max_height_ft", "max_speed_mph", "min_height_ft", "min_speed_mph", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_model_id", "updated_at") VALUES (NEW."created_at", NEW."description", NEW."distinguishing_features", NEW."id", NEW."max_height_ft", NEW."max_speed_mph", NEW."min_height_ft", NEW."min_speed_mph", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."ride_model_id", NEW."updated_at"); RETURN NULL;', + hash="cc7f0da0cef685e4504f8cad28af9b296ed8a2aa", + operation="UPDATE", + pgid="pgtrigger_update_update_f7599", + table="rides_ridemodelvariant", + when="AFTER", + ), + ), + ), + migrations.AddIndex( + model_name="ridepaircomparison", + index=models.Index( + fields=["ride_a", "ride_b"], name="rides_ridep_ride_a__eb0674_idx" + ), + ), + migrations.AddIndex( + model_name="ridepaircomparison", + index=models.Index( + fields=["last_calculated"], name="rides_ridep_last_ca_bd9f6c_idx" + ), + ), + migrations.AlterUniqueTogether( + name="ridepaircomparison", + unique_together={("ride_a", "ride_b")}, + ), + pgtrigger.migrations.AddTrigger( + model_name="ridepaircomparison", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridepaircomparisonevent" ("id", "last_calculated", "mutual_riders_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_a_avg_rating", "ride_a_id", "ride_a_wins", "ride_b_avg_rating", "ride_b_id", "ride_b_wins", "ties") VALUES (NEW."id", NEW."last_calculated", NEW."mutual_riders_count", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."ride_a_avg_rating", NEW."ride_a_id", NEW."ride_a_wins", NEW."ride_b_avg_rating", NEW."ride_b_id", NEW."ride_b_wins", NEW."ties"); RETURN NULL;', + hash="6a640e10fcfd58c48029ee5b84ea7f0826f50022", + operation="INSERT", + pgid="pgtrigger_insert_insert_9ad59", + table="rides_ridepaircomparison", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridepaircomparison", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridepaircomparisonevent" ("id", "last_calculated", "mutual_riders_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_a_avg_rating", "ride_a_id", "ride_a_wins", "ride_b_avg_rating", "ride_b_id", "ride_b_wins", "ties") VALUES (NEW."id", NEW."last_calculated", NEW."mutual_riders_count", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."ride_a_avg_rating", NEW."ride_a_id", NEW."ride_a_wins", NEW."ride_b_avg_rating", NEW."ride_b_id", NEW."ride_b_wins", NEW."ties"); RETURN NULL;', + hash="a77eee0b791bada3f84f008dabd7486c66b03fa6", + operation="UPDATE", + pgid="pgtrigger_update_update_73b31", + table="rides_ridepaircomparison", + when="AFTER", + ), + ), + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["ride", "is_primary"], name="rides_ridep_ride_id_aa49f1_idx" + ), + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["ride", "is_approved"], name="rides_ridep_ride_id_f1eddc_idx" + ), + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["ride", "photo_type"], name="rides_ridep_ride_id_49e7ec_idx" + ), + ), + migrations.AddIndex( + model_name="ridephoto", + index=models.Index( + fields=["created_at"], name="rides_ridep_created_106e02_idx" + ), + ), + migrations.AddConstraint( + model_name="ridephoto", + constraint=models.UniqueConstraint( + condition=models.Q(("is_primary", True)), + fields=("ride",), + name="unique_primary_ride_photo", + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridephoto", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridephotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image_id", "is_approved", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "ride_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image_id", NEW."is_approved", NEW."is_primary", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photo_type", NEW."ride_id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', + hash="51487ac871d9d90c75f695f106e5f1f43fdb00c6", + operation="INSERT", + pgid="pgtrigger_insert_insert_0043a", + table="rides_ridephoto", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridephoto", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridephotoevent" ("alt_text", "caption", "created_at", "date_taken", "id", "image_id", "is_approved", "is_primary", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo_type", "ride_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."created_at", NEW."date_taken", NEW."id", NEW."image_id", NEW."is_approved", NEW."is_primary", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photo_type", NEW."ride_id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;', + hash="6147489f087c144f887386548cba269ffc193094", + operation="UPDATE", + pgid="pgtrigger_update_update_93a7e", + table="rides_ridephoto", + when="AFTER", + ), + ), + ), + migrations.AddConstraint( + model_name="ride", + constraint=models.CheckConstraint( + condition=models.Q( + ("closing_date__isnull", True), + ("opening_date__isnull", True), + ("closing_date__gte", models.F("opening_date")), + _connector="OR", + ), + name="ride_closing_after_opening", + violation_error_message="Closing date must be after opening date", + ), + ), + migrations.AddConstraint( + model_name="ride", + constraint=models.CheckConstraint( + condition=models.Q( + ("min_height_in__isnull", True), + ("max_height_in__isnull", True), + ("min_height_in__lte", models.F("max_height_in")), + _connector="OR", + ), + name="ride_height_requirements_logical", + violation_error_message="Minimum height cannot exceed maximum height", + ), + ), + migrations.AddConstraint( + model_name="ride", + constraint=models.CheckConstraint( + condition=models.Q( + ("min_height_in__isnull", True), + models.Q(("min_height_in__gte", 30), ("min_height_in__lte", 90)), + _connector="OR", + ), + name="ride_min_height_reasonable", + violation_error_message="Minimum height must be between 30 and 90 inches", + ), + ), + migrations.AddConstraint( + model_name="ride", + constraint=models.CheckConstraint( + condition=models.Q( + ("max_height_in__isnull", True), + models.Q(("max_height_in__gte", 30), ("max_height_in__lte", 90)), + _connector="OR", + ), + name="ride_max_height_reasonable", + violation_error_message="Maximum height must be between 30 and 90 inches", + ), + ), + migrations.AddConstraint( + model_name="ride", + constraint=models.CheckConstraint( + condition=models.Q( + ("average_rating__isnull", True), + models.Q(("average_rating__gte", 1), ("average_rating__lte", 10)), + _connector="OR", + ), + name="ride_rating_range", + violation_error_message="Average rating must be between 1 and 10", + ), + ), + migrations.AddConstraint( + model_name="ride", + constraint=models.CheckConstraint( + condition=models.Q( + ("capacity_per_hour__isnull", True), + ("capacity_per_hour__gt", 0), + _connector="OR", + ), + name="ride_capacity_positive", + violation_error_message="Hourly capacity must be positive", + ), + ), + migrations.AddConstraint( + model_name="ride", + constraint=models.CheckConstraint( + condition=models.Q( + ("ride_duration_seconds__isnull", True), + ("ride_duration_seconds__gt", 0), + _connector="OR", + ), + name="ride_duration_positive", + violation_error_message="Ride duration must be positive", + ), + ), + migrations.AlterUniqueTogether( + name="ride", + unique_together={("park", "slug")}, + ), + 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", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "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."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_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="64e055c574495c0f09b3cbfb12442d4e4113e4f2", + 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", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "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."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_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="6476c8dd4bbb0e2ae42ca2daa5c691b87f9119e9", + operation="UPDATE", + pgid="pgtrigger_update_update_4917a", + table="rides_ride", + when="AFTER", + ), + ), + ), + migrations.AddIndex( + model_name="rideranking", + index=models.Index(fields=["rank"], name="rides_rider_rank_ea4706_idx"), + ), + migrations.AddIndex( + model_name="rideranking", + index=models.Index( + fields=["winning_percentage", "-mutual_riders_count"], + name="rides_rider_winning_d9b3e8_idx", + ), + ), + migrations.AddIndex( + model_name="rideranking", + index=models.Index( + fields=["ride", "last_calculated"], + name="rides_rider_ride_id_ece73d_idx", + ), + ), + migrations.AddConstraint( + model_name="rideranking", + constraint=models.CheckConstraint( + condition=models.Q( + ("winning_percentage__gte", 0), ("winning_percentage__lte", 1) + ), + name="rideranking_winning_percentage_range", + violation_error_message="Winning percentage must be between 0 and 1", + ), + ), + migrations.AddConstraint( + model_name="rideranking", + constraint=models.CheckConstraint( + condition=models.Q( + ("average_rating__isnull", True), + models.Q(("average_rating__gte", 1), ("average_rating__lte", 10)), + _connector="OR", + ), + name="rideranking_average_rating_range", + violation_error_message="Average rating must be between 1 and 10", + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="rideranking", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_riderankingevent" ("average_rating", "calculation_version", "comparison_count", "id", "last_calculated", "losses", "mutual_riders_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "ride_id", "ties", "winning_percentage", "wins") VALUES (NEW."average_rating", NEW."calculation_version", NEW."comparison_count", NEW."id", NEW."last_calculated", NEW."losses", NEW."mutual_riders_count", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rank", NEW."ride_id", NEW."ties", NEW."winning_percentage", NEW."wins"); RETURN NULL;', + hash="c5f9dced5824a55e6f36e476eb382ed770aa5716", + operation="INSERT", + pgid="pgtrigger_insert_insert_01af3", + table="rides_rideranking", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="rideranking", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_riderankingevent" ("average_rating", "calculation_version", "comparison_count", "id", "last_calculated", "losses", "mutual_riders_count", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "ride_id", "ties", "winning_percentage", "wins") VALUES (NEW."average_rating", NEW."calculation_version", NEW."comparison_count", NEW."id", NEW."last_calculated", NEW."losses", NEW."mutual_riders_count", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rank", NEW."ride_id", NEW."ties", NEW."winning_percentage", NEW."wins"); RETURN NULL;', + hash="363e44ce3c87e8b66406d63d6f1b26ad604c79d2", + operation="UPDATE", + pgid="pgtrigger_update_update_c3f27", + table="rides_rideranking", + when="AFTER", + ), + ), + ), + migrations.AddConstraint( + model_name="ridereview", + constraint=models.CheckConstraint( + condition=models.Q(("rating__gte", 1), ("rating__lte", 10)), + name="ride_review_rating_range", + violation_error_message="Rating must be between 1 and 10", + ), + ), + migrations.AddConstraint( + model_name="ridereview", + constraint=models.CheckConstraint( + condition=models.Q( + ("visit_date__lte", django.db.models.functions.datetime.Now()) + ), + name="ride_review_visit_date_not_future", + violation_error_message="Visit date cannot be in the future", + ), + ), + migrations.AddConstraint( + model_name="ridereview", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q( + ("moderated_at__isnull", True), ("moderated_by__isnull", True) + ), + models.Q( + ("moderated_at__isnull", False), ("moderated_by__isnull", False) + ), + _connector="OR", + ), + name="ride_review_moderation_consistency", + violation_error_message="Moderated reviews must have both moderator and moderation timestamp", + ), + ), + migrations.AlterUniqueTogether( + name="ridereview", + unique_together={("ride", "user")}, + ), + pgtrigger.migrations.AddTrigger( + model_name="ridereview", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_ridereviewevent" ("content", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "ride_id", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rating", NEW."ride_id", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;', + hash="0d6021859fef528429d7d6028439c08de6040e52", + operation="INSERT", + pgid="pgtrigger_insert_insert_33237", + table="rides_ridereview", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ridereview", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_ridereviewevent" ("content", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "ride_id", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rating", NEW."ride_id", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;', + hash="b0acd6ed16f909a42f9bedd975c7f385d0868d32", + operation="UPDATE", + pgid="pgtrigger_update_update_90298", + table="rides_ridereview", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="rollercoasterstats", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_rollercoasterstatsevent" ("cars_per_train", "height_ft", "id", "inversions", "length_ft", "max_drop_height_ft", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "propulsion_system", "ride_id", "ride_time_seconds", "roller_coaster_type", "seats_per_car", "speed_mph", "track_material", "track_type", "train_style", "trains_count") VALUES (NEW."cars_per_train", NEW."height_ft", NEW."id", NEW."inversions", NEW."length_ft", NEW."max_drop_height_ft", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."propulsion_system", NEW."ride_id", NEW."ride_time_seconds", NEW."roller_coaster_type", NEW."seats_per_car", NEW."speed_mph", NEW."track_material", NEW."track_type", NEW."train_style", NEW."trains_count"); RETURN NULL;', + hash="89e2bb56c0befa025a9961f8df34a8a02c09f188", + operation="INSERT", + pgid="pgtrigger_insert_insert_96f8b", + table="rides_rollercoasterstats", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="rollercoasterstats", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_rollercoasterstatsevent" ("cars_per_train", "height_ft", "id", "inversions", "length_ft", "max_drop_height_ft", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "propulsion_system", "ride_id", "ride_time_seconds", "roller_coaster_type", "seats_per_car", "speed_mph", "track_material", "track_type", "train_style", "trains_count") VALUES (NEW."cars_per_train", NEW."height_ft", NEW."id", NEW."inversions", NEW."length_ft", NEW."max_drop_height_ft", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."propulsion_system", NEW."ride_id", NEW."ride_time_seconds", NEW."roller_coaster_type", NEW."seats_per_car", NEW."speed_mph", NEW."track_material", NEW."track_type", NEW."train_style", NEW."trains_count"); RETURN NULL;', + hash="047cc99ae3282202b6dc43c8dbe07690076d5068", + operation="UPDATE", + pgid="pgtrigger_update_update_24e8a", + table="rides_rollercoasterstats", + when="AFTER", + ), + ), + ), + ] diff --git a/backend/apps/rides/migrations/__init__.py b/backend/apps/rides/migrations/__init__.py new file mode 100644 index 00000000..e69de29b