From fe960e8b62a89ab5ef00a300880e246d4d4ae3a0 Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:44:37 -0500 Subject: [PATCH] w --- backend/apps/api/v1/core/urls.py | 11 +- .../apps/core/api/milestone_serializers.py | 93 ++++ backend/apps/core/api/milestone_views.py | 79 ++++ .../migrations/0010_add_milestone_model.py | 94 ++++ backend/apps/core/models.py | 112 +++++ backend/apps/moderation/models.py | 5 +- backend/apps/moderation/views.py | 142 ++++++ ...rce_url_is_test_data_and_date_precision.py | 117 +++++ .../migrations/0030_company_schema_parity.py | 72 +++ backend/apps/parks/models/companies.py | 50 +- backend/apps/parks/models/parks.py | 38 +- .../0034_add_ride_category_fields.py | 432 ++++++++++++++++++ .../0035_add_company_and_ridemodel_fields.py | 119 +++++ .../0036_add_remaining_parity_fields.py | 87 ++++ ...rce_url_is_test_data_and_date_precision.py | 107 +++++ .../migrations/0038_company_schema_parity.py | 51 +++ backend/apps/rides/models/company.py | 71 +++ backend/apps/rides/models/rides.py | 309 ++++++++++++- backend/apps/rides/signals.py | 2 +- .../apps/rides/tests/test_ride_workflows.py | 76 +-- backend/celerybeat-schedule-wal | Bin 243112 -> 2616232 bytes 21 files changed, 2008 insertions(+), 59 deletions(-) create mode 100644 backend/apps/core/api/milestone_serializers.py create mode 100644 backend/apps/core/api/milestone_views.py create mode 100644 backend/apps/core/migrations/0010_add_milestone_model.py create mode 100644 backend/apps/parks/migrations/0029_add_source_url_is_test_data_and_date_precision.py create mode 100644 backend/apps/parks/migrations/0030_company_schema_parity.py create mode 100644 backend/apps/rides/migrations/0034_add_ride_category_fields.py create mode 100644 backend/apps/rides/migrations/0035_add_company_and_ridemodel_fields.py create mode 100644 backend/apps/rides/migrations/0036_add_remaining_parity_fields.py create mode 100644 backend/apps/rides/migrations/0037_add_source_url_is_test_data_and_date_precision.py create mode 100644 backend/apps/rides/migrations/0038_company_schema_parity.py diff --git a/backend/apps/api/v1/core/urls.py b/backend/apps/api/v1/core/urls.py index d11e5304..be05d51e 100644 --- a/backend/apps/api/v1/core/urls.py +++ b/backend/apps/api/v1/core/urls.py @@ -3,9 +3,15 @@ Core API URL configuration. Centralized from apps.core.urls """ -from django.urls import path +from django.urls import include, path +from rest_framework.routers import DefaultRouter from . import views +from apps.core.api.milestone_views import MilestoneViewSet + +# Create router for viewsets +router = DefaultRouter() +router.register(r"milestones", MilestoneViewSet, basename="milestone") # Entity search endpoints - migrated from apps.core.urls urlpatterns = [ @@ -30,4 +36,7 @@ urlpatterns = [ views.TelemetryView.as_view(), name="telemetry", ), + # Include router URLs (milestones, etc.) + path("", include(router.urls)), ] + diff --git a/backend/apps/core/api/milestone_serializers.py b/backend/apps/core/api/milestone_serializers.py new file mode 100644 index 00000000..cbbb25a5 --- /dev/null +++ b/backend/apps/core/api/milestone_serializers.py @@ -0,0 +1,93 @@ +""" +Milestone serializers for timeline events. +""" + +from rest_framework import serializers + +from apps.core.models import Milestone + + +class MilestoneSerializer(serializers.ModelSerializer): + """Serializer for Milestone model matching frontend milestoneValidationSchema.""" + + class Meta: + model = Milestone + fields = [ + "id", + "title", + "description", + "event_type", + "event_date", + "event_date_precision", + "entity_type", + "entity_id", + "is_public", + "display_order", + "from_value", + "to_value", + "from_entity_id", + "to_entity_id", + "from_location_id", + "to_location_id", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "created_at", "updated_at"] + + +class MilestoneCreateSerializer(serializers.ModelSerializer): + """Serializer for creating milestones.""" + + class Meta: + model = Milestone + fields = [ + "title", + "description", + "event_type", + "event_date", + "event_date_precision", + "entity_type", + "entity_id", + "is_public", + "display_order", + "from_value", + "to_value", + "from_entity_id", + "to_entity_id", + "from_location_id", + "to_location_id", + ] + + def validate(self, attrs): + """Validate change events have from/to values.""" + change_events = ["name_change", "operator_change", "owner_change", "location_change", "status_change"] + if attrs.get("event_type") in change_events: + has_change_data = ( + attrs.get("from_value") + or attrs.get("to_value") + or attrs.get("from_entity_id") + or attrs.get("to_entity_id") + or attrs.get("from_location_id") + or attrs.get("to_location_id") + ) + if not has_change_data: + raise serializers.ValidationError( + "Change events must specify what changed (from/to values or entity IDs)" + ) + return attrs + + +class MilestoneListSerializer(serializers.ModelSerializer): + """Lightweight serializer for listing milestones.""" + + class Meta: + model = Milestone + fields = [ + "id", + "title", + "event_type", + "event_date", + "entity_type", + "entity_id", + "is_public", + ] diff --git a/backend/apps/core/api/milestone_views.py b/backend/apps/core/api/milestone_views.py new file mode 100644 index 00000000..516a46da --- /dev/null +++ b/backend/apps/core/api/milestone_views.py @@ -0,0 +1,79 @@ +""" +Milestone views for timeline events. +""" + +from django_filters import rest_framework as filters +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from rest_framework.response import Response + +from apps.core.models import Milestone + +from .milestone_serializers import ( + MilestoneCreateSerializer, + MilestoneListSerializer, + MilestoneSerializer, +) + + +class MilestoneFilter(filters.FilterSet): + """Filters for milestone listing.""" + + entity_type = filters.CharFilter(field_name="entity_type") + entity_id = filters.UUIDFilter(field_name="entity_id") + event_type = filters.CharFilter(field_name="event_type") + is_public = filters.BooleanFilter(field_name="is_public") + event_date_after = filters.DateFilter(field_name="event_date", lookup_expr="gte") + event_date_before = filters.DateFilter(field_name="event_date", lookup_expr="lte") + + class Meta: + model = Milestone + fields = ["entity_type", "entity_id", "event_type", "is_public"] + + +class MilestoneViewSet(viewsets.ModelViewSet): + """ + ViewSet for managing milestones/timeline events. + + Supports filtering by entity_type, entity_id, event_type, and date range. + """ + + queryset = Milestone.objects.all() + filterset_class = MilestoneFilter + permission_classes = [IsAuthenticatedOrReadOnly] + + def get_serializer_class(self): + if self.action == "list": + return MilestoneListSerializer + if self.action == "create": + return MilestoneCreateSerializer + return MilestoneSerializer + + def get_queryset(self): + """Filter queryset based on visibility.""" + queryset = super().get_queryset() + + # Non-authenticated users only see public milestones + if not self.request.user.is_authenticated: + queryset = queryset.filter(is_public=True) + + return queryset.order_by("-event_date", "display_order") + + @action(detail=False, methods=["get"], url_path="entity/(?P[^/]+)/(?P[^/]+)") + def by_entity(self, request, entity_type=None, entity_id=None): + """Get all milestones for a specific entity.""" + queryset = self.get_queryset().filter( + entity_type=entity_type, + entity_id=entity_id, + ) + serializer = MilestoneListSerializer(queryset, many=True) + return Response(serializer.data) + + @action(detail=False, methods=["get"], url_path="timeline") + def timeline(self, request): + """Get a unified timeline view of recent milestones across all entities.""" + limit = int(request.query_params.get("limit", 50)) + queryset = self.get_queryset()[:limit] + serializer = MilestoneListSerializer(queryset, many=True) + return Response(serializer.data) diff --git a/backend/apps/core/migrations/0010_add_milestone_model.py b/backend/apps/core/migrations/0010_add_milestone_model.py new file mode 100644 index 00000000..371449a4 --- /dev/null +++ b/backend/apps/core/migrations/0010_add_milestone_model.py @@ -0,0 +1,94 @@ +# Generated by Django 5.2.9 on 2026-01-08 17:59 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_pageview_pageviewevent_and_more'), + ('pghistory', '0007_auto_20250421_0444'), + ] + + operations = [ + migrations.CreateModel( + name='MilestoneEvent', + 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)), + ('title', models.CharField(help_text='Title or name of the event', max_length=200)), + ('description', models.TextField(blank=True, help_text='Detailed description of the event')), + ('event_type', models.CharField(help_text="Type of event (e.g., 'opening', 'closing', 'name_change', 'status_change')", max_length=50)), + ('event_date', models.DateField(help_text='Date when the event occurred or will occur')), + ('event_date_precision', models.CharField(choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the event date', max_length=20)), + ('entity_type', models.CharField(help_text="Type of entity (e.g., 'park', 'ride', 'company')", max_length=50)), + ('entity_id', models.UUIDField(help_text='UUID of the associated entity')), + ('is_public', models.BooleanField(default=True, help_text='Whether this milestone is publicly visible')), + ('display_order', models.IntegerField(default=0, help_text='Order for displaying multiple milestones on the same date')), + ('from_value', models.CharField(blank=True, help_text='Previous value (for change events)', max_length=200)), + ('to_value', models.CharField(blank=True, help_text='New value (for change events)', max_length=200)), + ('from_entity_id', models.UUIDField(blank=True, help_text='Previous entity reference (e.g., old operator)', null=True)), + ('to_entity_id', models.UUIDField(blank=True, help_text='New entity reference (e.g., new operator)', null=True)), + ('from_location_id', models.UUIDField(blank=True, help_text='Previous location reference (for relocations)', null=True)), + ('to_location_id', models.UUIDField(blank=True, help_text='New location reference (for relocations)', null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Milestone', + 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)), + ('title', models.CharField(help_text='Title or name of the event', max_length=200)), + ('description', models.TextField(blank=True, help_text='Detailed description of the event')), + ('event_type', models.CharField(db_index=True, help_text="Type of event (e.g., 'opening', 'closing', 'name_change', 'status_change')", max_length=50)), + ('event_date', models.DateField(db_index=True, help_text='Date when the event occurred or will occur')), + ('event_date_precision', models.CharField(choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the event date', max_length=20)), + ('entity_type', models.CharField(db_index=True, help_text="Type of entity (e.g., 'park', 'ride', 'company')", max_length=50)), + ('entity_id', models.UUIDField(db_index=True, help_text='UUID of the associated entity')), + ('is_public', models.BooleanField(default=True, help_text='Whether this milestone is publicly visible')), + ('display_order', models.IntegerField(default=0, help_text='Order for displaying multiple milestones on the same date')), + ('from_value', models.CharField(blank=True, help_text='Previous value (for change events)', max_length=200)), + ('to_value', models.CharField(blank=True, help_text='New value (for change events)', max_length=200)), + ('from_entity_id', models.UUIDField(blank=True, help_text='Previous entity reference (e.g., old operator)', null=True)), + ('to_entity_id', models.UUIDField(blank=True, help_text='New entity reference (e.g., new operator)', null=True)), + ('from_location_id', models.UUIDField(blank=True, help_text='Previous location reference (for relocations)', null=True)), + ('to_location_id', models.UUIDField(blank=True, help_text='New location reference (for relocations)', null=True)), + ], + options={ + 'verbose_name': 'Milestone', + 'verbose_name_plural': 'Milestones', + 'ordering': ['-event_date', 'display_order'], + 'abstract': False, + 'indexes': [models.Index(fields=['entity_type', 'entity_id'], name='core_milest_entity__effdde_idx'), models.Index(fields=['event_type', 'event_date'], name='core_milest_event_t_0070b8_idx'), models.Index(fields=['is_public', 'event_date'], name='core_milest_is_publ_2ce98c_idx')], + }, + ), + pgtrigger.migrations.AddTrigger( + model_name='milestone', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "core_milestoneevent" ("created_at", "description", "display_order", "entity_id", "entity_type", "event_date", "event_date_precision", "event_type", "from_entity_id", "from_location_id", "from_value", "id", "is_public", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "title", "to_entity_id", "to_location_id", "to_value", "updated_at") VALUES (NEW."created_at", NEW."description", NEW."display_order", NEW."entity_id", NEW."entity_type", NEW."event_date", NEW."event_date_precision", NEW."event_type", NEW."from_entity_id", NEW."from_location_id", NEW."from_value", NEW."id", NEW."is_public", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."title", NEW."to_entity_id", NEW."to_location_id", NEW."to_value", NEW."updated_at"); RETURN NULL;', hash='6c4386ed0356cf9a3db65c829163401409e79622', operation='INSERT', pgid='pgtrigger_insert_insert_52c81', table='core_milestone', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='milestone', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "core_milestoneevent" ("created_at", "description", "display_order", "entity_id", "entity_type", "event_date", "event_date_precision", "event_type", "from_entity_id", "from_location_id", "from_value", "id", "is_public", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "title", "to_entity_id", "to_location_id", "to_value", "updated_at") VALUES (NEW."created_at", NEW."description", NEW."display_order", NEW."entity_id", NEW."entity_type", NEW."event_date", NEW."event_date_precision", NEW."event_type", NEW."from_entity_id", NEW."from_location_id", NEW."from_value", NEW."id", NEW."is_public", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."title", NEW."to_entity_id", NEW."to_location_id", NEW."to_value", NEW."updated_at"); RETURN NULL;', hash='fafe30b7266d1d1a0a2b3486f5b7e713a8252f97', operation='UPDATE', pgid='pgtrigger_update_update_0209b', table='core_milestone', when='AFTER')), + ), + migrations.AddField( + model_name='milestoneevent', + 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='milestoneevent', + name='pgh_obj', + field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='events', to='core.milestone'), + ), + ] diff --git a/backend/apps/core/models.py b/backend/apps/core/models.py index 380b707b..6d5a0a1b 100644 --- a/backend/apps/core/models.py +++ b/backend/apps/core/models.py @@ -1049,3 +1049,115 @@ class ApprovalTransactionMetric(models.Model): status = "✓" if self.success else "✗" return f"{status} Submission {self.submission_id[:8]} by {self.moderator_id[:8]}" + +@pghistory.track() +class Milestone(TrackedModel): + """ + Timeline event / milestone for any entity. + + Supports various event types like openings, closures, name changes, + operator changes, and other significant events. Uses a generic + entity reference pattern to work with Parks, Rides, Companies, etc. + + Maps to frontend milestoneValidationSchema in entityValidationSchemas.ts + """ + + class DatePrecision(models.TextChoices): + EXACT = "exact", "Exact Date" + MONTH = "month", "Month and Year" + YEAR = "year", "Year Only" + DECADE = "decade", "Decade" + CENTURY = "century", "Century" + APPROXIMATE = "approximate", "Approximate" + + # Core event information + title = models.CharField( + max_length=200, + help_text="Title or name of the event", + ) + description = models.TextField( + blank=True, + help_text="Detailed description of the event", + ) + event_type = models.CharField( + max_length=50, + db_index=True, + help_text="Type of event (e.g., 'opening', 'closing', 'name_change', 'status_change')", + ) + event_date = models.DateField( + db_index=True, + help_text="Date when the event occurred or will occur", + ) + event_date_precision = models.CharField( + max_length=20, + choices=DatePrecision.choices, + default=DatePrecision.EXACT, + help_text="Precision of the event date", + ) + + # Generic entity reference + entity_type = models.CharField( + max_length=50, + db_index=True, + help_text="Type of entity (e.g., 'park', 'ride', 'company')", + ) + entity_id = models.UUIDField( + db_index=True, + help_text="UUID of the associated entity", + ) + + # Display settings + is_public = models.BooleanField( + default=True, + help_text="Whether this milestone is publicly visible", + ) + display_order = models.IntegerField( + default=0, + help_text="Order for displaying multiple milestones on the same date", + ) + + # Change tracking fields (for name_change, operator_change, etc.) + from_value = models.CharField( + max_length=200, + blank=True, + help_text="Previous value (for change events)", + ) + to_value = models.CharField( + max_length=200, + blank=True, + help_text="New value (for change events)", + ) + from_entity_id = models.UUIDField( + null=True, + blank=True, + help_text="Previous entity reference (e.g., old operator)", + ) + to_entity_id = models.UUIDField( + null=True, + blank=True, + help_text="New entity reference (e.g., new operator)", + ) + from_location_id = models.UUIDField( + null=True, + blank=True, + help_text="Previous location reference (for relocations)", + ) + to_location_id = models.UUIDField( + null=True, + blank=True, + help_text="New location reference (for relocations)", + ) + + class Meta(TrackedModel.Meta): + ordering = ["-event_date", "display_order"] + verbose_name = "Milestone" + verbose_name_plural = "Milestones" + indexes = [ + models.Index(fields=["entity_type", "entity_id"]), + models.Index(fields=["event_type", "event_date"]), + models.Index(fields=["is_public", "event_date"]), + ] + + def __str__(self) -> str: + return f"{self.title} ({self.event_date})" + diff --git a/backend/apps/moderation/models.py b/backend/apps/moderation/models.py index 9c6ab9ea..7ee47522 100644 --- a/backend/apps/moderation/models.py +++ b/backend/apps/moderation/models.py @@ -864,12 +864,13 @@ class PhotoSubmission(StateMachineMixin, TrackedModel): self.save() def auto_approve(self) -> None: - """Auto - approve submissions from moderators""" + """Auto-approve submissions from moderators.""" # Get user role safely user_role = getattr(self.user, "role", None) - # If user is moderator or above, auto-approve + # If user is moderator or above, claim then approve if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]: + self.claim(user=self.user) self.approve(self.user) def escalate(self, moderator: UserType = None, notes: str = "", user=None) -> None: diff --git a/backend/apps/moderation/views.py b/backend/apps/moderation/views.py index c8cc146a..18422ac8 100644 --- a/backend/apps/moderation/views.py +++ b/backend/apps/moderation/views.py @@ -1718,6 +1718,148 @@ class EditSubmissionViewSet(viewsets.ModelViewSet): except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + @action(detail=False, methods=["post"], permission_classes=[IsModeratorOrAdmin], url_path="release-expired") + def release_expired_locks(self, request): + """ + Release all expired claim locks. + + This is typically handled by a Celery task, but can be triggered manually. + Claims are expired after 30 minutes by default. + """ + from datetime import timedelta + + expiry_threshold = timezone.now() - timedelta(minutes=30) + + expired_claims = EditSubmission.objects.filter( + status="CLAIMED", + claimed_at__lt=expiry_threshold + ) + + released_count = 0 + for submission in expired_claims: + submission.status = "PENDING" + submission.claimed_by = None + submission.claimed_at = None + submission.save(update_fields=["status", "claimed_by", "claimed_at"]) + released_count += 1 + + return Response({ + "released_count": released_count, + "message": f"Released {released_count} expired lock(s)" + }) + + @action(detail=True, methods=["post"], permission_classes=[IsAdminOrSuperuser], url_path="admin-release") + def admin_release(self, request, pk=None): + """ + Admin/superuser force release of a specific claim. + """ + submission = self.get_object() + + if submission.status != "CLAIMED": + return Response( + {"error": "Submission is not claimed"}, + status=status.HTTP_400_BAD_REQUEST + ) + + submission.status = "PENDING" + submission.claimed_by = None + submission.claimed_at = None + submission.save(update_fields=["status", "claimed_by", "claimed_at"]) + + return Response({ + "success": True, + "message": f"Lock released on submission {submission.id}" + }) + + @action(detail=False, methods=["post"], permission_classes=[IsAdminOrSuperuser], url_path="admin-release-all") + def admin_release_all(self, request): + """ + Admin/superuser force release of all active claims. + """ + claimed_submissions = EditSubmission.objects.filter(status="CLAIMED") + + released_count = 0 + for submission in claimed_submissions: + submission.status = "PENDING" + submission.claimed_by = None + submission.claimed_at = None + submission.save(update_fields=["status", "claimed_by", "claimed_at"]) + released_count += 1 + + return Response({ + "released_count": released_count, + "message": f"Released all {released_count} active lock(s)" + }) + + @action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin], url_path="reassign") + def reassign(self, request, pk=None): + """ + Reassign a submission to a different moderator. + + Only admins can reassign submissions claimed by other moderators. + The submission must be in CLAIMED status. + """ + submission = self.get_object() + new_moderator_id = request.data.get("new_moderator_id") + + if not new_moderator_id: + return Response( + {"error": "new_moderator_id is required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + new_moderator = User.objects.get(pk=new_moderator_id) + except User.DoesNotExist: + return Response( + {"error": "Moderator not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + # Check moderator permissions + if new_moderator.role not in ["MODERATOR", "ADMIN", "SUPERUSER"]: + return Response( + {"error": "User is not a moderator"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Update the claim + submission.claimed_by = new_moderator + submission.claimed_at = timezone.now() + submission.save(update_fields=["claimed_by", "claimed_at"]) + + return Response({ + "success": True, + "message": f"Submission reassigned to {new_moderator.username}" + }) + + @action(detail=False, methods=["post"], permission_classes=[IsModeratorOrAdmin], url_path="audit-log") + def log_admin_action(self, request): + """ + Log an admin action for audit trail. + + This creates an audit log entry for moderator actions. + """ + action_type = request.data.get("action_type", "") + action_details = request.data.get("action_details", {}) + target_entity = request.data.get("target_entity", {}) + + # Create audit log entry + logger.info( + f"[AdminAction] User {request.user.username} - {action_type}", + extra={ + "user_id": request.user.id, + "action_type": action_type, + "action_details": action_details, + "target_entity": target_entity, + } + ) + + return Response({ + "success": True, + "message": "Action logged successfully" + }) + @action(detail=False, methods=["get"], permission_classes=[IsModeratorOrAdmin], url_path="my-active-claim") def my_active_claim(self, request): """ diff --git a/backend/apps/parks/migrations/0029_add_source_url_is_test_data_and_date_precision.py b/backend/apps/parks/migrations/0029_add_source_url_is_test_data_and_date_precision.py new file mode 100644 index 00000000..4b417a72 --- /dev/null +++ b/backend/apps/parks/migrations/0029_add_source_url_is_test_data_and_date_precision.py @@ -0,0 +1,117 @@ +# Generated by Django 5.2.9 on 2026-01-08 18:05 + +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parks', '0028_add_date_precision_fields'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='park', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='park', + name='update_update', + ), + migrations.AddField( + model_name='company', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='company', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, Wikipedia)'), + ), + migrations.AddField( + model_name='companyevent', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='companyevent', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, Wikipedia)'), + ), + migrations.AddField( + model_name='park', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='park', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, Wikipedia)'), + ), + migrations.AddField( + model_name='parkevent', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='parkevent', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, Wikipedia)'), + ), + migrations.AlterField( + model_name='company', + name='founded_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], help_text='Precision of the founding date', max_length=20), + ), + migrations.AlterField( + model_name='companyevent', + name='founded_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], help_text='Precision of the founding date', max_length=20), + ), + migrations.AlterField( + model_name='park', + name='closing_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the closing date', max_length=20), + ), + migrations.AlterField( + model_name='park', + name='opening_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the opening date', max_length=20), + ), + migrations.AlterField( + model_name='parkevent', + name='closing_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the closing date', max_length=20), + ), + migrations.AlterField( + model_name='parkevent', + name='opening_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the opening date', max_length=20), + ), + pgtrigger.migrations.AddTrigger( + model_name='company', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "parks_companyevent" ("average_rating", "banner_image_url", "card_image_url", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "id", "is_test_data", "logo_url", "name", "parks_count", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "review_count", "rides_count", "roles", "slug", "source_url", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."banner_image_url", NEW."card_image_url", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."id", NEW."is_test_data", NEW."logo_url", NEW."name", NEW."parks_count", NEW."person_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."review_count", NEW."rides_count", NEW."roles", NEW."slug", NEW."source_url", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;', hash='8352ecabfefc26dab2c91be68a9e137a1e48cbd2', operation='INSERT', pgid='pgtrigger_insert_insert_35b57', table='parks_company', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='company', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "parks_companyevent" ("average_rating", "banner_image_url", "card_image_url", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "id", "is_test_data", "logo_url", "name", "parks_count", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "review_count", "rides_count", "roles", "slug", "source_url", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."banner_image_url", NEW."card_image_url", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."id", NEW."is_test_data", NEW."logo_url", NEW."name", NEW."parks_count", NEW."person_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."review_count", NEW."rides_count", NEW."roles", NEW."slug", NEW."source_url", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;', hash='5d8b399ed7573fa0d5411042902c0a494785e071', operation='UPDATE', pgid='pgtrigger_update_update_d3286', table='parks_company', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='park', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "parks_parkevent" ("average_rating", "banner_image_id", "card_image_id", "closing_date", "closing_date_precision", "coaster_count", "created_at", "description", "email", "id", "is_test_data", "name", "opening_date", "opening_date_precision", "opening_year", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "phone", "property_owner_id", "ride_count", "search_text", "size_acres", "slug", "source_url", "status", "timezone", "updated_at", "url", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."card_image_id", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_count", NEW."created_at", NEW."description", NEW."email", NEW."id", NEW."is_test_data", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."operating_season", NEW."operator_id", NEW."park_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."phone", NEW."property_owner_id", NEW."ride_count", NEW."search_text", NEW."size_acres", NEW."slug", NEW."source_url", NEW."status", NEW."timezone", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', hash='cb0e4e056880e2e6febc5a0905a437e56dab89de', operation='INSERT', pgid='pgtrigger_insert_insert_66883', table='parks_park', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='park', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "parks_parkevent" ("average_rating", "banner_image_id", "card_image_id", "closing_date", "closing_date_precision", "coaster_count", "created_at", "description", "email", "id", "is_test_data", "name", "opening_date", "opening_date_precision", "opening_year", "operating_season", "operator_id", "park_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "phone", "property_owner_id", "ride_count", "search_text", "size_acres", "slug", "source_url", "status", "timezone", "updated_at", "url", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."card_image_id", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_count", NEW."created_at", NEW."description", NEW."email", NEW."id", NEW."is_test_data", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."operating_season", NEW."operator_id", NEW."park_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."phone", NEW."property_owner_id", NEW."ride_count", NEW."search_text", NEW."size_acres", NEW."slug", NEW."source_url", NEW."status", NEW."timezone", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', hash='dd10d0b79ed3bf1caca8d4ffb520cd0be298bc0d', operation='UPDATE', pgid='pgtrigger_update_update_19f56', table='parks_park', when='AFTER')), + ), + ] diff --git a/backend/apps/parks/migrations/0030_company_schema_parity.py b/backend/apps/parks/migrations/0030_company_schema_parity.py new file mode 100644 index 00000000..f93a57f2 --- /dev/null +++ b/backend/apps/parks/migrations/0030_company_schema_parity.py @@ -0,0 +1,72 @@ +# Generated by Django 5.2.9 on 2026-01-08 18:20 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parks', '0029_add_source_url_is_test_data_and_date_precision'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='update_update', + ), + migrations.AddField( + model_name='company', + name='banner_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for banner image', max_length=255), + ), + migrations.AddField( + model_name='company', + name='card_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for card image', max_length=255), + ), + migrations.AddField( + model_name='company', + name='headquarters_location', + field=models.CharField(blank=True, help_text="Headquarters location description (e.g., 'Los Angeles, CA, USA')", max_length=200), + ), + migrations.AddField( + model_name='company', + name='location', + field=models.ForeignKey(blank=True, help_text='Linked location record for headquarters', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='companies_hq', to='parks.parklocation'), + ), + migrations.AddField( + model_name='companyevent', + name='banner_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for banner image', max_length=255), + ), + migrations.AddField( + model_name='companyevent', + name='card_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for card image', max_length=255), + ), + migrations.AddField( + model_name='companyevent', + name='headquarters_location', + field=models.CharField(blank=True, help_text="Headquarters location description (e.g., 'Los Angeles, CA, USA')", max_length=200), + ), + migrations.AddField( + model_name='companyevent', + name='location', + field=models.ForeignKey(blank=True, db_constraint=False, help_text='Linked location record for headquarters', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='parks.parklocation'), + ), + pgtrigger.migrations.AddTrigger( + model_name='company', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "parks_companyevent" ("average_rating", "banner_image_id", "banner_image_url", "card_image_id", "card_image_url", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "is_test_data", "location_id", "logo_url", "name", "parks_count", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "review_count", "rides_count", "roles", "slug", "source_url", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."banner_image_url", NEW."card_image_id", NEW."card_image_url", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."is_test_data", NEW."location_id", NEW."logo_url", NEW."name", NEW."parks_count", NEW."person_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."review_count", NEW."rides_count", NEW."roles", NEW."slug", NEW."source_url", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;', hash='9e3f8a98696e2655ada53342a59b11a71bfa384c', operation='INSERT', pgid='pgtrigger_insert_insert_35b57', table='parks_company', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='company', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "parks_companyevent" ("average_rating", "banner_image_id", "banner_image_url", "card_image_id", "card_image_url", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "is_test_data", "location_id", "logo_url", "name", "parks_count", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "review_count", "rides_count", "roles", "slug", "source_url", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."banner_image_id", NEW."banner_image_url", NEW."card_image_id", NEW."card_image_url", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."is_test_data", NEW."location_id", NEW."logo_url", NEW."name", NEW."parks_count", NEW."person_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."review_count", NEW."rides_count", NEW."roles", NEW."slug", NEW."source_url", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;', hash='953a919e1969082370e189b0b47a2ce3fc9dafcf', operation='UPDATE', pgid='pgtrigger_update_update_d3286', table='parks_company', when='AFTER')), + ), + ] diff --git a/backend/apps/parks/models/companies.py b/backend/apps/parks/models/companies.py index 368ea951..34d45c30 100644 --- a/backend/apps/parks/models/companies.py +++ b/backend/apps/parks/models/companies.py @@ -62,12 +62,15 @@ class Company(TrackedModel): founded_year = models.PositiveIntegerField(blank=True, null=True, help_text="Year the company was founded") founded_date = models.DateField(blank=True, null=True, help_text="Full founding date if known") DATE_PRECISION_CHOICES = [ - ("YEAR", "Year only"), - ("MONTH", "Month and year"), - ("DAY", "Full date"), + ("exact", "Exact Date"), + ("month", "Month and Year"), + ("year", "Year Only"), + ("decade", "Decade"), + ("century", "Century"), + ("approximate", "Approximate"), ] founded_date_precision = models.CharField( - max_length=10, + max_length=20, choices=DATE_PRECISION_CHOICES, blank=True, help_text="Precision of the founding date", @@ -78,6 +81,35 @@ class Company(TrackedModel): banner_image_url = models.URLField(blank=True, help_text="Banner image for company page header") card_image_url = models.URLField(blank=True, help_text="Card/thumbnail image for listings") + # Image ID fields (for frontend submissions - Cloudflare image IDs) + banner_image_id = models.CharField( + max_length=255, + blank=True, + help_text="Cloudflare image ID for banner image", + ) + card_image_id = models.CharField( + max_length=255, + blank=True, + help_text="Cloudflare image ID for card image", + ) + + # Location relationship (for headquarters coordinates) + location = models.ForeignKey( + "ParkLocation", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="companies_hq", + help_text="Linked location record for headquarters", + ) + + # Text-based headquarters location (matches frontend schema) + headquarters_location = models.CharField( + max_length=200, + blank=True, + help_text="Headquarters location description (e.g., 'Los Angeles, CA, USA')", + ) + # Rating & Review Aggregates (computed fields, updated by triggers/signals) average_rating = models.DecimalField( max_digits=3, @@ -95,6 +127,16 @@ class Company(TrackedModel): parks_count = models.IntegerField(default=0, help_text="Number of parks operated (auto-calculated)") rides_count = models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)") + # Submission metadata fields (from frontend schema) + source_url = models.URLField( + blank=True, + help_text="Source URL for the data (e.g., official website, Wikipedia)", + ) + is_test_data = models.BooleanField( + default=False, + help_text="Whether this is test/development data", + ) + def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) diff --git a/backend/apps/parks/models/parks.py b/backend/apps/parks/models/parks.py index c24f3e29..62c16469 100644 --- a/backend/apps/parks/models/parks.py +++ b/backend/apps/parks/models/parks.py @@ -55,17 +55,31 @@ class Park(StateMachineMixin, TrackedModel): # Details opening_date = models.DateField(null=True, blank=True, help_text="Opening date") opening_date_precision = models.CharField( - max_length=10, - choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")], - default="DAY", + max_length=20, + choices=[ + ("exact", "Exact Date"), + ("month", "Month and Year"), + ("year", "Year Only"), + ("decade", "Decade"), + ("century", "Century"), + ("approximate", "Approximate"), + ], + default="exact", blank=True, - help_text="Precision of the opening date (YEAR for circa dates)", + help_text="Precision of the opening date", ) closing_date = models.DateField(null=True, blank=True, help_text="Closing date") closing_date_precision = models.CharField( - max_length=10, - choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")], - default="DAY", + max_length=20, + choices=[ + ("exact", "Exact Date"), + ("month", "Month and Year"), + ("year", "Year Only"), + ("decade", "Decade"), + ("century", "Century"), + ("approximate", "Approximate"), + ], + default="exact", blank=True, help_text="Precision of the closing date", ) @@ -146,6 +160,16 @@ class Park(StateMachineMixin, TrackedModel): help_text="Timezone identifier for park operations (e.g., 'America/New_York')", ) + # Submission metadata fields (from frontend schema) + source_url = models.URLField( + blank=True, + help_text="Source URL for the data (e.g., official website, Wikipedia)", + ) + is_test_data = models.BooleanField( + default=False, + help_text="Whether this is test/development data", + ) + class Meta: verbose_name = "Park" verbose_name_plural = "Parks" diff --git a/backend/apps/rides/migrations/0034_add_ride_category_fields.py b/backend/apps/rides/migrations/0034_add_ride_category_fields.py new file mode 100644 index 00000000..2786ebdf --- /dev/null +++ b/backend/apps/rides/migrations/0034_add_ride_category_fields.py @@ -0,0 +1,432 @@ +# Generated by Django 5.2.9 on 2026-01-07 20:30 + +import django.contrib.postgres.fields +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rides', '0033_add_ride_subtype_and_age'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='ride', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ride', + name='update_update', + ), + migrations.AddField( + model_name='ride', + name='animatronics_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of animatronic figures', null=True), + ), + migrations.AddField( + model_name='ride', + name='arm_length_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Length of ride arm in meters', max_digits=5, null=True), + ), + migrations.AddField( + model_name='ride', + name='boat_capacity', + field=models.PositiveIntegerField(blank=True, help_text='Number of passengers per boat/vehicle', null=True), + ), + migrations.AddField( + model_name='ride', + name='character_theme', + field=models.CharField(blank=True, help_text='Character or IP theme (e.g., Paw Patrol, Sesame Street)', max_length=200), + ), + migrations.AddField( + model_name='ride', + name='coaster_type', + field=models.CharField(blank=True, help_text='Coaster structure type: steel, wood, or hybrid', max_length=20), + ), + migrations.AddField( + model_name='ride', + name='drop_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum drop height in meters', max_digits=6, null=True), + ), + migrations.AddField( + model_name='ride', + name='educational_theme', + field=models.CharField(blank=True, help_text='Educational or learning theme if applicable', max_length=200), + ), + migrations.AddField( + model_name='ride', + name='flume_type', + field=models.CharField(blank=True, help_text='Type of flume or water channel', max_length=100), + ), + migrations.AddField( + model_name='ride', + name='gforce_max', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum G-force experienced', max_digits=4, null=True), + ), + migrations.AddField( + model_name='ride', + name='height_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Height of the ride structure in meters', max_digits=6, null=True), + ), + migrations.AddField( + model_name='ride', + name='intensity_level', + field=models.CharField(blank=True, help_text='Intensity classification: family, thrill, or extreme', max_length=20), + ), + migrations.AddField( + model_name='ride', + name='length_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Total track/ride length in meters', max_digits=8, null=True), + ), + migrations.AddField( + model_name='ride', + name='max_age', + field=models.PositiveIntegerField(blank=True, help_text='Maximum recommended age in years', null=True), + ), + migrations.AddField( + model_name='ride', + name='max_height_reached_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum height reached during ride cycle in meters', max_digits=6, null=True), + ), + migrations.AddField( + model_name='ride', + name='max_speed_kmh', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum speed in kilometers per hour', max_digits=6, null=True), + ), + migrations.AddField( + model_name='ride', + name='min_age', + field=models.PositiveIntegerField(blank=True, help_text='Minimum recommended age in years', null=True), + ), + migrations.AddField( + model_name='ride', + name='motion_pattern', + field=models.CharField(blank=True, help_text="Description of the ride's motion pattern", max_length=200), + ), + migrations.AddField( + model_name='ride', + name='platform_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of ride platforms or gondolas', null=True), + ), + migrations.AddField( + model_name='ride', + name='projection_type', + field=models.CharField(blank=True, help_text='Type of projection technology used', max_length=100), + ), + migrations.AddField( + model_name='ride', + name='propulsion_method', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text="Propulsion methods (e.g., ['chain_lift', 'lsm'])", size=None), + ), + migrations.AddField( + model_name='ride', + name='ride_system', + field=models.CharField(blank=True, help_text='Ride system type (e.g., trackless, omnimover)', max_length=100), + ), + migrations.AddField( + model_name='ride', + name='rotation_speed_rpm', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Rotation speed in revolutions per minute', max_digits=6, null=True), + ), + migrations.AddField( + model_name='ride', + name='rotation_type', + field=models.CharField(blank=True, help_text='Rotation axis: horizontal, vertical, multi_axis, pendulum, or none', max_length=20), + ), + migrations.AddField( + model_name='ride', + name='round_trip_duration_seconds', + field=models.PositiveIntegerField(blank=True, help_text='Duration of a complete round trip in seconds', null=True), + ), + migrations.AddField( + model_name='ride', + name='route_length_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Total route length in meters', max_digits=8, null=True), + ), + migrations.AddField( + model_name='ride', + name='scenes_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of distinct scenes or show sections', null=True), + ), + migrations.AddField( + model_name='ride', + name='seating_type', + field=models.CharField(blank=True, help_text='Seating configuration: sit_down, inverted, flying, stand_up, etc.', max_length=20), + ), + migrations.AddField( + model_name='ride', + name='show_duration_seconds', + field=models.PositiveIntegerField(blank=True, help_text='Duration of show elements in seconds', null=True), + ), + migrations.AddField( + model_name='ride', + name='splash_height_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum splash height in meters', max_digits=5, null=True), + ), + migrations.AddField( + model_name='ride', + name='stations_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of stations or stops', null=True), + ), + migrations.AddField( + model_name='ride', + name='story_description', + field=models.TextField(blank=True, help_text='Narrative or story description for the ride'), + ), + migrations.AddField( + model_name='ride', + name='support_material', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text='Support structure material types', size=None), + ), + migrations.AddField( + model_name='ride', + name='swing_angle_degrees', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum swing angle in degrees', max_digits=5, null=True), + ), + migrations.AddField( + model_name='ride', + name='theme_name', + field=models.CharField(blank=True, help_text='Primary theme or IP name', max_length=200), + ), + migrations.AddField( + model_name='ride', + name='track_material', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text="Track material types (e.g., ['steel', 'wood'])", size=None), + ), + migrations.AddField( + model_name='ride', + name='transport_type', + field=models.CharField(blank=True, help_text='Transport mode: train, monorail, skylift, ferry, peoplemover, or cable_car', max_length=20), + ), + migrations.AddField( + model_name='ride', + name='vehicle_capacity', + field=models.PositiveIntegerField(blank=True, help_text='Passenger capacity per vehicle', null=True), + ), + migrations.AddField( + model_name='ride', + name='vehicles_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of vehicles in operation', null=True), + ), + migrations.AddField( + model_name='ride', + name='water_depth_cm', + field=models.PositiveIntegerField(blank=True, help_text='Water depth in centimeters', null=True), + ), + migrations.AddField( + model_name='ride', + name='wetness_level', + field=models.CharField(blank=True, help_text='Expected wetness: dry, light, moderate, or soaked', max_length=20), + ), + migrations.AddField( + model_name='rideevent', + name='animatronics_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of animatronic figures', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='arm_length_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Length of ride arm in meters', max_digits=5, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='boat_capacity', + field=models.PositiveIntegerField(blank=True, help_text='Number of passengers per boat/vehicle', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='character_theme', + field=models.CharField(blank=True, help_text='Character or IP theme (e.g., Paw Patrol, Sesame Street)', max_length=200), + ), + migrations.AddField( + model_name='rideevent', + name='coaster_type', + field=models.CharField(blank=True, help_text='Coaster structure type: steel, wood, or hybrid', max_length=20), + ), + migrations.AddField( + model_name='rideevent', + name='drop_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum drop height in meters', max_digits=6, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='educational_theme', + field=models.CharField(blank=True, help_text='Educational or learning theme if applicable', max_length=200), + ), + migrations.AddField( + model_name='rideevent', + name='flume_type', + field=models.CharField(blank=True, help_text='Type of flume or water channel', max_length=100), + ), + migrations.AddField( + model_name='rideevent', + name='gforce_max', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum G-force experienced', max_digits=4, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='height_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Height of the ride structure in meters', max_digits=6, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='intensity_level', + field=models.CharField(blank=True, help_text='Intensity classification: family, thrill, or extreme', max_length=20), + ), + migrations.AddField( + model_name='rideevent', + name='length_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Total track/ride length in meters', max_digits=8, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='max_age', + field=models.PositiveIntegerField(blank=True, help_text='Maximum recommended age in years', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='max_height_reached_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum height reached during ride cycle in meters', max_digits=6, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='max_speed_kmh', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum speed in kilometers per hour', max_digits=6, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='min_age', + field=models.PositiveIntegerField(blank=True, help_text='Minimum recommended age in years', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='motion_pattern', + field=models.CharField(blank=True, help_text="Description of the ride's motion pattern", max_length=200), + ), + migrations.AddField( + model_name='rideevent', + name='platform_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of ride platforms or gondolas', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='projection_type', + field=models.CharField(blank=True, help_text='Type of projection technology used', max_length=100), + ), + migrations.AddField( + model_name='rideevent', + name='propulsion_method', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text="Propulsion methods (e.g., ['chain_lift', 'lsm'])", size=None), + ), + migrations.AddField( + model_name='rideevent', + name='ride_system', + field=models.CharField(blank=True, help_text='Ride system type (e.g., trackless, omnimover)', max_length=100), + ), + migrations.AddField( + model_name='rideevent', + name='rotation_speed_rpm', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Rotation speed in revolutions per minute', max_digits=6, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='rotation_type', + field=models.CharField(blank=True, help_text='Rotation axis: horizontal, vertical, multi_axis, pendulum, or none', max_length=20), + ), + migrations.AddField( + model_name='rideevent', + name='round_trip_duration_seconds', + field=models.PositiveIntegerField(blank=True, help_text='Duration of a complete round trip in seconds', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='route_length_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Total route length in meters', max_digits=8, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='scenes_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of distinct scenes or show sections', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='seating_type', + field=models.CharField(blank=True, help_text='Seating configuration: sit_down, inverted, flying, stand_up, etc.', max_length=20), + ), + migrations.AddField( + model_name='rideevent', + name='show_duration_seconds', + field=models.PositiveIntegerField(blank=True, help_text='Duration of show elements in seconds', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='splash_height_meters', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum splash height in meters', max_digits=5, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='stations_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of stations or stops', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='story_description', + field=models.TextField(blank=True, help_text='Narrative or story description for the ride'), + ), + migrations.AddField( + model_name='rideevent', + name='support_material', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text='Support structure material types', size=None), + ), + migrations.AddField( + model_name='rideevent', + name='swing_angle_degrees', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Maximum swing angle in degrees', max_digits=5, null=True), + ), + migrations.AddField( + model_name='rideevent', + name='theme_name', + field=models.CharField(blank=True, help_text='Primary theme or IP name', max_length=200), + ), + migrations.AddField( + model_name='rideevent', + name='track_material', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), blank=True, default=list, help_text="Track material types (e.g., ['steel', 'wood'])", size=None), + ), + migrations.AddField( + model_name='rideevent', + name='transport_type', + field=models.CharField(blank=True, help_text='Transport mode: train, monorail, skylift, ferry, peoplemover, or cable_car', max_length=20), + ), + migrations.AddField( + model_name='rideevent', + name='vehicle_capacity', + field=models.PositiveIntegerField(blank=True, help_text='Passenger capacity per vehicle', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='vehicles_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of vehicles in operation', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='water_depth_cm', + field=models.PositiveIntegerField(blank=True, help_text='Water depth in centimeters', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='wetness_level', + field=models.CharField(blank=True, help_text='Expected wetness: dry, light, moderate, or soaked', max_length=20), + ), + pgtrigger.migrations.AddTrigger( + model_name='ride', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "rides_rideevent" ("age_requirement", "animatronics_count", "arm_length_meters", "average_rating", "banner_image_id", "boat_capacity", "capacity_per_hour", "card_image_id", "category", "character_theme", "closing_date", "closing_date_precision", "coaster_type", "created_at", "description", "designer_id", "drop_meters", "educational_theme", "flume_type", "gforce_max", "height_meters", "id", "intensity_level", "length_meters", "manufacturer_id", "max_age", "max_height_in", "max_height_reached_meters", "max_speed_kmh", "min_age", "min_height_in", "motion_pattern", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_count", "post_closing_status", "projection_type", "propulsion_method", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "ride_system", "rotation_speed_rpm", "rotation_type", "round_trip_duration_seconds", "route_length_meters", "scenes_count", "search_text", "seating_type", "show_duration_seconds", "slug", "splash_height_meters", "stations_count", "status", "status_since", "story_description", "support_material", "swing_angle_degrees", "theme_name", "track_material", "transport_type", "updated_at", "url", "vehicle_capacity", "vehicles_count", "water_depth_cm", "wetness_level") VALUES (NEW."age_requirement", NEW."animatronics_count", NEW."arm_length_meters", NEW."average_rating", NEW."banner_image_id", NEW."boat_capacity", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."character_theme", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_type", NEW."created_at", NEW."description", NEW."designer_id", NEW."drop_meters", NEW."educational_theme", NEW."flume_type", NEW."gforce_max", NEW."height_meters", NEW."id", NEW."intensity_level", NEW."length_meters", NEW."manufacturer_id", NEW."max_age", NEW."max_height_in", NEW."max_height_reached_meters", NEW."max_speed_kmh", NEW."min_age", NEW."min_height_in", NEW."motion_pattern", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."platform_count", NEW."post_closing_status", NEW."projection_type", NEW."propulsion_method", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."ride_system", NEW."rotation_speed_rpm", NEW."rotation_type", NEW."round_trip_duration_seconds", NEW."route_length_meters", NEW."scenes_count", NEW."search_text", NEW."seating_type", NEW."show_duration_seconds", NEW."slug", NEW."splash_height_meters", NEW."stations_count", NEW."status", NEW."status_since", NEW."story_description", NEW."support_material", NEW."swing_angle_degrees", NEW."theme_name", NEW."track_material", NEW."transport_type", NEW."updated_at", NEW."url", NEW."vehicle_capacity", NEW."vehicles_count", NEW."water_depth_cm", NEW."wetness_level"); RETURN NULL;', hash='0515185b26eb9635e7b7f7d52cfa87b90636c409', operation='INSERT', pgid='pgtrigger_insert_insert_52074', table='rides_ride', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='ride', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "rides_rideevent" ("age_requirement", "animatronics_count", "arm_length_meters", "average_rating", "banner_image_id", "boat_capacity", "capacity_per_hour", "card_image_id", "category", "character_theme", "closing_date", "closing_date_precision", "coaster_type", "created_at", "description", "designer_id", "drop_meters", "educational_theme", "flume_type", "gforce_max", "height_meters", "id", "intensity_level", "length_meters", "manufacturer_id", "max_age", "max_height_in", "max_height_reached_meters", "max_speed_kmh", "min_age", "min_height_in", "motion_pattern", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_count", "post_closing_status", "projection_type", "propulsion_method", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "ride_system", "rotation_speed_rpm", "rotation_type", "round_trip_duration_seconds", "route_length_meters", "scenes_count", "search_text", "seating_type", "show_duration_seconds", "slug", "splash_height_meters", "stations_count", "status", "status_since", "story_description", "support_material", "swing_angle_degrees", "theme_name", "track_material", "transport_type", "updated_at", "url", "vehicle_capacity", "vehicles_count", "water_depth_cm", "wetness_level") VALUES (NEW."age_requirement", NEW."animatronics_count", NEW."arm_length_meters", NEW."average_rating", NEW."banner_image_id", NEW."boat_capacity", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."character_theme", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_type", NEW."created_at", NEW."description", NEW."designer_id", NEW."drop_meters", NEW."educational_theme", NEW."flume_type", NEW."gforce_max", NEW."height_meters", NEW."id", NEW."intensity_level", NEW."length_meters", NEW."manufacturer_id", NEW."max_age", NEW."max_height_in", NEW."max_height_reached_meters", NEW."max_speed_kmh", NEW."min_age", NEW."min_height_in", NEW."motion_pattern", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."platform_count", NEW."post_closing_status", NEW."projection_type", NEW."propulsion_method", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."ride_system", NEW."rotation_speed_rpm", NEW."rotation_type", NEW."round_trip_duration_seconds", NEW."route_length_meters", NEW."scenes_count", NEW."search_text", NEW."seating_type", NEW."show_duration_seconds", NEW."slug", NEW."splash_height_meters", NEW."stations_count", NEW."status", NEW."status_since", NEW."story_description", NEW."support_material", NEW."swing_angle_degrees", NEW."theme_name", NEW."track_material", NEW."transport_type", NEW."updated_at", NEW."url", NEW."vehicle_capacity", NEW."vehicles_count", NEW."water_depth_cm", NEW."wetness_level"); RETURN NULL;', hash='e0bb5999b75a6d10f73651cba99c40e06bb2b49c', operation='UPDATE', pgid='pgtrigger_update_update_4917a', table='rides_ride', when='AFTER')), + ), + ] diff --git a/backend/apps/rides/migrations/0035_add_company_and_ridemodel_fields.py b/backend/apps/rides/migrations/0035_add_company_and_ridemodel_fields.py new file mode 100644 index 00000000..478aba40 --- /dev/null +++ b/backend/apps/rides/migrations/0035_add_company_and_ridemodel_fields.py @@ -0,0 +1,119 @@ +# Generated by Django 5.2.9 on 2026-01-07 21:01 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parks', '0028_add_date_precision_fields'), + ('rides', '0034_add_ride_category_fields'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ridemodel', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ridemodel', + name='update_update', + ), + migrations.AddField( + model_name='company', + name='banner_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for banner image', max_length=255), + ), + migrations.AddField( + model_name='company', + name='card_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for card image', max_length=255), + ), + migrations.AddField( + model_name='company', + name='founded_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact'), ('month', 'Month'), ('year', 'Year'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='', help_text='Precision of the founded date', max_length=20), + ), + migrations.AddField( + model_name='company', + name='founded_year', + field=models.PositiveIntegerField(blank=True, help_text='Year the company was founded (alternative to founded_date)', null=True), + ), + migrations.AddField( + model_name='company', + name='headquarters_location', + field=models.CharField(blank=True, help_text="Headquarters location description (e.g., 'Los Angeles, CA, USA')", max_length=200), + ), + migrations.AddField( + model_name='company', + name='location', + field=models.ForeignKey(blank=True, help_text='Linked location record for headquarters', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='companies', to='parks.parklocation'), + ), + migrations.AddField( + model_name='companyevent', + name='banner_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for banner image', max_length=255), + ), + migrations.AddField( + model_name='companyevent', + name='card_image_id', + field=models.CharField(blank=True, help_text='Cloudflare image ID for card image', max_length=255), + ), + migrations.AddField( + model_name='companyevent', + name='founded_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact'), ('month', 'Month'), ('year', 'Year'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='', help_text='Precision of the founded date', max_length=20), + ), + migrations.AddField( + model_name='companyevent', + name='founded_year', + field=models.PositiveIntegerField(blank=True, help_text='Year the company was founded (alternative to founded_date)', null=True), + ), + migrations.AddField( + model_name='companyevent', + name='headquarters_location', + field=models.CharField(blank=True, help_text="Headquarters location description (e.g., 'Los Angeles, CA, USA')", max_length=200), + ), + migrations.AddField( + model_name='companyevent', + name='location', + field=models.ForeignKey(blank=True, db_constraint=False, help_text='Linked location record for headquarters', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='parks.parklocation'), + ), + migrations.AddField( + model_name='ridemodel', + name='ride_type', + field=models.CharField(blank=True, help_text="Specific ride type within the category (e.g., 'Flying Coaster', 'Inverted Coaster')", max_length=100), + ), + migrations.AddField( + model_name='ridemodelevent', + name='ride_type', + field=models.CharField(blank=True, help_text="Specific ride type within the category (e.g., 'Flying Coaster', 'Inverted Coaster')", max_length=100), + ), + pgtrigger.migrations.AddTrigger( + model_name='company', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "rides_companyevent" ("banner_image_id", "card_image_id", "coasters_count", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "location_id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "url", "website") VALUES (NEW."banner_image_id", NEW."card_image_id", NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."location_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='d1efc807d08a85e448a3294e70abb85e1c9c40ff', 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" ("banner_image_id", "card_image_id", "coasters_count", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "location_id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "url", "website") VALUES (NEW."banner_image_id", NEW."card_image_id", NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."location_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='dd4644183deefdfa27ec6d282c6da0c09d4df927', operation='UPDATE', pgid='pgtrigger_update_update_456a8', table='rides_company', when='AFTER')), + ), + 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", "ride_type", "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."ride_type", 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='715219f75d39aa2d59ffe836084dab943a322c5f', 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", "ride_type", "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."ride_type", 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='4f1d59b4ef9ddd207f7e4a56843d830ab67cff38', operation='UPDATE', pgid='pgtrigger_update_update_0ca1a', table='rides_ridemodel', when='AFTER')), + ), + ] diff --git a/backend/apps/rides/migrations/0036_add_remaining_parity_fields.py b/backend/apps/rides/migrations/0036_add_remaining_parity_fields.py new file mode 100644 index 00000000..e82752b1 --- /dev/null +++ b/backend/apps/rides/migrations/0036_add_remaining_parity_fields.py @@ -0,0 +1,87 @@ +# Generated by Django 5.2.9 on 2026-01-08 01:40 + +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rides', '0035_add_company_and_ridemodel_fields'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ride', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ride', + name='update_update', + ), + migrations.AddField( + model_name='company', + name='person_type', + field=models.CharField(blank=True, choices=[('company', 'Company'), ('individual', 'Individual'), ('firm', 'Firm'), ('organization', 'Organization')], default='company', help_text='Type of entity (company, individual, firm, organization)', max_length=20), + ), + migrations.AddField( + model_name='companyevent', + name='person_type', + field=models.CharField(blank=True, choices=[('company', 'Company'), ('individual', 'Individual'), ('firm', 'Firm'), ('organization', 'Organization')], default='company', help_text='Type of entity (company, individual, firm, organization)', max_length=20), + ), + migrations.AddField( + model_name='ride', + name='duration_seconds', + field=models.PositiveIntegerField(blank=True, help_text='Ride duration in seconds', null=True), + ), + migrations.AddField( + model_name='ride', + name='height_requirement_cm', + field=models.PositiveIntegerField(blank=True, help_text='Minimum height requirement in centimeters', null=True), + ), + migrations.AddField( + model_name='ride', + name='inversions_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of inversions (for coasters)', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='duration_seconds', + field=models.PositiveIntegerField(blank=True, help_text='Ride duration in seconds', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='height_requirement_cm', + field=models.PositiveIntegerField(blank=True, help_text='Minimum height requirement in centimeters', null=True), + ), + migrations.AddField( + model_name='rideevent', + name='inversions_count', + field=models.PositiveIntegerField(blank=True, help_text='Number of inversions (for coasters)', null=True), + ), + pgtrigger.migrations.AddTrigger( + model_name='company', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "rides_companyevent" ("banner_image_id", "card_image_id", "coasters_count", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "location_id", "name", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "url", "website") VALUES (NEW."banner_image_id", NEW."card_image_id", NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."location_id", NEW."name", NEW."person_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', hash='636ad62fbef5026486e8eb22d7b3ad3a08b08972', 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" ("banner_image_id", "card_image_id", "coasters_count", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "location_id", "name", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "updated_at", "url", "website") VALUES (NEW."banner_image_id", NEW."card_image_id", NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."location_id", NEW."name", NEW."person_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', hash='d0c405cab0f8f61aa24dd2074fd615a56fcc812a', operation='UPDATE', pgid='pgtrigger_update_update_456a8', table='rides_company', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='ride', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "rides_rideevent" ("age_requirement", "animatronics_count", "arm_length_meters", "average_rating", "banner_image_id", "boat_capacity", "capacity_per_hour", "card_image_id", "category", "character_theme", "closing_date", "closing_date_precision", "coaster_type", "created_at", "description", "designer_id", "drop_meters", "duration_seconds", "educational_theme", "flume_type", "gforce_max", "height_meters", "height_requirement_cm", "id", "intensity_level", "inversions_count", "length_meters", "manufacturer_id", "max_age", "max_height_in", "max_height_reached_meters", "max_speed_kmh", "min_age", "min_height_in", "motion_pattern", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_count", "post_closing_status", "projection_type", "propulsion_method", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "ride_system", "rotation_speed_rpm", "rotation_type", "round_trip_duration_seconds", "route_length_meters", "scenes_count", "search_text", "seating_type", "show_duration_seconds", "slug", "splash_height_meters", "stations_count", "status", "status_since", "story_description", "support_material", "swing_angle_degrees", "theme_name", "track_material", "transport_type", "updated_at", "url", "vehicle_capacity", "vehicles_count", "water_depth_cm", "wetness_level") VALUES (NEW."age_requirement", NEW."animatronics_count", NEW."arm_length_meters", NEW."average_rating", NEW."banner_image_id", NEW."boat_capacity", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."character_theme", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_type", NEW."created_at", NEW."description", NEW."designer_id", NEW."drop_meters", NEW."duration_seconds", NEW."educational_theme", NEW."flume_type", NEW."gforce_max", NEW."height_meters", NEW."height_requirement_cm", NEW."id", NEW."intensity_level", NEW."inversions_count", NEW."length_meters", NEW."manufacturer_id", NEW."max_age", NEW."max_height_in", NEW."max_height_reached_meters", NEW."max_speed_kmh", NEW."min_age", NEW."min_height_in", NEW."motion_pattern", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."platform_count", NEW."post_closing_status", NEW."projection_type", NEW."propulsion_method", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."ride_system", NEW."rotation_speed_rpm", NEW."rotation_type", NEW."round_trip_duration_seconds", NEW."route_length_meters", NEW."scenes_count", NEW."search_text", NEW."seating_type", NEW."show_duration_seconds", NEW."slug", NEW."splash_height_meters", NEW."stations_count", NEW."status", NEW."status_since", NEW."story_description", NEW."support_material", NEW."swing_angle_degrees", NEW."theme_name", NEW."track_material", NEW."transport_type", NEW."updated_at", NEW."url", NEW."vehicle_capacity", NEW."vehicles_count", NEW."water_depth_cm", NEW."wetness_level"); RETURN NULL;', hash='db6754d5334c498976180acdf6f2dd7c043cb9c1', operation='INSERT', pgid='pgtrigger_insert_insert_52074', table='rides_ride', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='ride', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "rides_rideevent" ("age_requirement", "animatronics_count", "arm_length_meters", "average_rating", "banner_image_id", "boat_capacity", "capacity_per_hour", "card_image_id", "category", "character_theme", "closing_date", "closing_date_precision", "coaster_type", "created_at", "description", "designer_id", "drop_meters", "duration_seconds", "educational_theme", "flume_type", "gforce_max", "height_meters", "height_requirement_cm", "id", "intensity_level", "inversions_count", "length_meters", "manufacturer_id", "max_age", "max_height_in", "max_height_reached_meters", "max_speed_kmh", "min_age", "min_height_in", "motion_pattern", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_count", "post_closing_status", "projection_type", "propulsion_method", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "ride_system", "rotation_speed_rpm", "rotation_type", "round_trip_duration_seconds", "route_length_meters", "scenes_count", "search_text", "seating_type", "show_duration_seconds", "slug", "splash_height_meters", "stations_count", "status", "status_since", "story_description", "support_material", "swing_angle_degrees", "theme_name", "track_material", "transport_type", "updated_at", "url", "vehicle_capacity", "vehicles_count", "water_depth_cm", "wetness_level") VALUES (NEW."age_requirement", NEW."animatronics_count", NEW."arm_length_meters", NEW."average_rating", NEW."banner_image_id", NEW."boat_capacity", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."character_theme", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_type", NEW."created_at", NEW."description", NEW."designer_id", NEW."drop_meters", NEW."duration_seconds", NEW."educational_theme", NEW."flume_type", NEW."gforce_max", NEW."height_meters", NEW."height_requirement_cm", NEW."id", NEW."intensity_level", NEW."inversions_count", NEW."length_meters", NEW."manufacturer_id", NEW."max_age", NEW."max_height_in", NEW."max_height_reached_meters", NEW."max_speed_kmh", NEW."min_age", NEW."min_height_in", NEW."motion_pattern", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."platform_count", NEW."post_closing_status", NEW."projection_type", NEW."propulsion_method", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."ride_system", NEW."rotation_speed_rpm", NEW."rotation_type", NEW."round_trip_duration_seconds", NEW."route_length_meters", NEW."scenes_count", NEW."search_text", NEW."seating_type", NEW."show_duration_seconds", NEW."slug", NEW."splash_height_meters", NEW."stations_count", NEW."status", NEW."status_since", NEW."story_description", NEW."support_material", NEW."swing_angle_degrees", NEW."theme_name", NEW."track_material", NEW."transport_type", NEW."updated_at", NEW."url", NEW."vehicle_capacity", NEW."vehicles_count", NEW."water_depth_cm", NEW."wetness_level"); RETURN NULL;', hash='3bff6632dbf5e5fab62671b5c2da263fb4682611', operation='UPDATE', pgid='pgtrigger_update_update_4917a', table='rides_ride', when='AFTER')), + ), + ] diff --git a/backend/apps/rides/migrations/0037_add_source_url_is_test_data_and_date_precision.py b/backend/apps/rides/migrations/0037_add_source_url_is_test_data_and_date_precision.py new file mode 100644 index 00000000..adda36d0 --- /dev/null +++ b/backend/apps/rides/migrations/0037_add_source_url_is_test_data_and_date_precision.py @@ -0,0 +1,107 @@ +# Generated by Django 5.2.9 on 2026-01-08 18:05 + +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rides', '0036_add_remaining_parity_fields'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='ride', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ride', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ridemodel', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='ridemodel', + name='update_update', + ), + migrations.AddField( + model_name='ride', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='ride', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, RCDB)'), + ), + migrations.AddField( + model_name='rideevent', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='rideevent', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, RCDB)'), + ), + migrations.AddField( + model_name='ridemodel', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='ridemodel', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., manufacturer website)'), + ), + migrations.AddField( + model_name='ridemodelevent', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='ridemodelevent', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., manufacturer website)'), + ), + migrations.AlterField( + model_name='ride', + name='closing_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the closing date', max_length=20), + ), + migrations.AlterField( + model_name='ride', + name='opening_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the opening date', max_length=20), + ), + migrations.AlterField( + model_name='rideevent', + name='closing_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the closing date', max_length=20), + ), + migrations.AlterField( + model_name='rideevent', + name='opening_date_precision', + field=models.CharField(blank=True, choices=[('exact', 'Exact Date'), ('month', 'Month and Year'), ('year', 'Year Only'), ('decade', 'Decade'), ('century', 'Century'), ('approximate', 'Approximate')], default='exact', help_text='Precision of the opening date', max_length=20), + ), + pgtrigger.migrations.AddTrigger( + model_name='ride', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "rides_rideevent" ("age_requirement", "animatronics_count", "arm_length_meters", "average_rating", "banner_image_id", "boat_capacity", "capacity_per_hour", "card_image_id", "category", "character_theme", "closing_date", "closing_date_precision", "coaster_type", "created_at", "description", "designer_id", "drop_meters", "duration_seconds", "educational_theme", "flume_type", "gforce_max", "height_meters", "height_requirement_cm", "id", "intensity_level", "inversions_count", "is_test_data", "length_meters", "manufacturer_id", "max_age", "max_height_in", "max_height_reached_meters", "max_speed_kmh", "min_age", "min_height_in", "motion_pattern", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_count", "post_closing_status", "projection_type", "propulsion_method", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "ride_system", "rotation_speed_rpm", "rotation_type", "round_trip_duration_seconds", "route_length_meters", "scenes_count", "search_text", "seating_type", "show_duration_seconds", "slug", "source_url", "splash_height_meters", "stations_count", "status", "status_since", "story_description", "support_material", "swing_angle_degrees", "theme_name", "track_material", "transport_type", "updated_at", "url", "vehicle_capacity", "vehicles_count", "water_depth_cm", "wetness_level") VALUES (NEW."age_requirement", NEW."animatronics_count", NEW."arm_length_meters", NEW."average_rating", NEW."banner_image_id", NEW."boat_capacity", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."character_theme", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_type", NEW."created_at", NEW."description", NEW."designer_id", NEW."drop_meters", NEW."duration_seconds", NEW."educational_theme", NEW."flume_type", NEW."gforce_max", NEW."height_meters", NEW."height_requirement_cm", NEW."id", NEW."intensity_level", NEW."inversions_count", NEW."is_test_data", NEW."length_meters", NEW."manufacturer_id", NEW."max_age", NEW."max_height_in", NEW."max_height_reached_meters", NEW."max_speed_kmh", NEW."min_age", NEW."min_height_in", NEW."motion_pattern", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."platform_count", NEW."post_closing_status", NEW."projection_type", NEW."propulsion_method", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."ride_system", NEW."rotation_speed_rpm", NEW."rotation_type", NEW."round_trip_duration_seconds", NEW."route_length_meters", NEW."scenes_count", NEW."search_text", NEW."seating_type", NEW."show_duration_seconds", NEW."slug", NEW."source_url", NEW."splash_height_meters", NEW."stations_count", NEW."status", NEW."status_since", NEW."story_description", NEW."support_material", NEW."swing_angle_degrees", NEW."theme_name", NEW."track_material", NEW."transport_type", NEW."updated_at", NEW."url", NEW."vehicle_capacity", NEW."vehicles_count", NEW."water_depth_cm", NEW."wetness_level"); RETURN NULL;', hash='07c5cf95d16c49e08014c23a4e5e35f55292c869', operation='INSERT', pgid='pgtrigger_insert_insert_52074', table='rides_ride', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='ride', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "rides_rideevent" ("age_requirement", "animatronics_count", "arm_length_meters", "average_rating", "banner_image_id", "boat_capacity", "capacity_per_hour", "card_image_id", "category", "character_theme", "closing_date", "closing_date_precision", "coaster_type", "created_at", "description", "designer_id", "drop_meters", "duration_seconds", "educational_theme", "flume_type", "gforce_max", "height_meters", "height_requirement_cm", "id", "intensity_level", "inversions_count", "is_test_data", "length_meters", "manufacturer_id", "max_age", "max_height_in", "max_height_reached_meters", "max_speed_kmh", "min_age", "min_height_in", "motion_pattern", "name", "opening_date", "opening_date_precision", "opening_year", "park_area_id", "park_id", "park_url", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_count", "post_closing_status", "projection_type", "propulsion_method", "ride_duration_seconds", "ride_model_id", "ride_sub_type", "ride_system", "rotation_speed_rpm", "rotation_type", "round_trip_duration_seconds", "route_length_meters", "scenes_count", "search_text", "seating_type", "show_duration_seconds", "slug", "source_url", "splash_height_meters", "stations_count", "status", "status_since", "story_description", "support_material", "swing_angle_degrees", "theme_name", "track_material", "transport_type", "updated_at", "url", "vehicle_capacity", "vehicles_count", "water_depth_cm", "wetness_level") VALUES (NEW."age_requirement", NEW."animatronics_count", NEW."arm_length_meters", NEW."average_rating", NEW."banner_image_id", NEW."boat_capacity", NEW."capacity_per_hour", NEW."card_image_id", NEW."category", NEW."character_theme", NEW."closing_date", NEW."closing_date_precision", NEW."coaster_type", NEW."created_at", NEW."description", NEW."designer_id", NEW."drop_meters", NEW."duration_seconds", NEW."educational_theme", NEW."flume_type", NEW."gforce_max", NEW."height_meters", NEW."height_requirement_cm", NEW."id", NEW."intensity_level", NEW."inversions_count", NEW."is_test_data", NEW."length_meters", NEW."manufacturer_id", NEW."max_age", NEW."max_height_in", NEW."max_height_reached_meters", NEW."max_speed_kmh", NEW."min_age", NEW."min_height_in", NEW."motion_pattern", NEW."name", NEW."opening_date", NEW."opening_date_precision", NEW."opening_year", NEW."park_area_id", NEW."park_id", NEW."park_url", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."platform_count", NEW."post_closing_status", NEW."projection_type", NEW."propulsion_method", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."ride_sub_type", NEW."ride_system", NEW."rotation_speed_rpm", NEW."rotation_type", NEW."round_trip_duration_seconds", NEW."route_length_meters", NEW."scenes_count", NEW."search_text", NEW."seating_type", NEW."show_duration_seconds", NEW."slug", NEW."source_url", NEW."splash_height_meters", NEW."stations_count", NEW."status", NEW."status_since", NEW."story_description", NEW."support_material", NEW."swing_angle_degrees", NEW."theme_name", NEW."track_material", NEW."transport_type", NEW."updated_at", NEW."url", NEW."vehicle_capacity", NEW."vehicles_count", NEW."water_depth_cm", NEW."wetness_level"); RETURN NULL;', hash='dabf771ba40b71c4d91ad1b1ed97a9712578096c', operation='UPDATE', pgid='pgtrigger_update_update_4917a', table='rides_ride', when='AFTER')), + ), + 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", "is_test_data", "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", "ride_type", "slug", "source_url", "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."is_test_data", 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."ride_type", NEW."slug", NEW."source_url", 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='9cc07f0217f79924bae066b5b8f9e7d5f55e211c', 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", "is_test_data", "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", "ride_type", "slug", "source_url", "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."is_test_data", 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."ride_type", NEW."slug", NEW."source_url", 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='f9f826a678fc0ed93ab788206fdb724c5445e469', operation='UPDATE', pgid='pgtrigger_update_update_0ca1a', table='rides_ridemodel', when='AFTER')), + ), + ] diff --git a/backend/apps/rides/migrations/0038_company_schema_parity.py b/backend/apps/rides/migrations/0038_company_schema_parity.py new file mode 100644 index 00000000..d49f024d --- /dev/null +++ b/backend/apps/rides/migrations/0038_company_schema_parity.py @@ -0,0 +1,51 @@ +# Generated by Django 5.2.9 on 2026-01-08 18:20 + +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rides', '0037_add_source_url_is_test_data_and_date_precision'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='company', + name='update_update', + ), + migrations.AddField( + model_name='company', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='company', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, Wikipedia)'), + ), + migrations.AddField( + model_name='companyevent', + name='is_test_data', + field=models.BooleanField(default=False, help_text='Whether this is test/development data'), + ), + migrations.AddField( + model_name='companyevent', + name='source_url', + field=models.URLField(blank=True, help_text='Source URL for the data (e.g., official website, Wikipedia)'), + ), + pgtrigger.migrations.AddTrigger( + model_name='company', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "rides_companyevent" ("banner_image_id", "card_image_id", "coasters_count", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "is_test_data", "location_id", "name", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "source_url", "updated_at", "url", "website") VALUES (NEW."banner_image_id", NEW."card_image_id", NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."is_test_data", NEW."location_id", NEW."name", NEW."person_type", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."source_url", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', hash='26c30b4bcabc0661de7627f32a6b12d2ea9895ac', 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" ("banner_image_id", "card_image_id", "coasters_count", "created_at", "description", "founded_date", "founded_date_precision", "founded_year", "headquarters_location", "id", "is_test_data", "location_id", "name", "person_type", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rides_count", "roles", "slug", "source_url", "updated_at", "url", "website") VALUES (NEW."banner_image_id", NEW."card_image_id", NEW."coasters_count", NEW."created_at", NEW."description", NEW."founded_date", NEW."founded_date_precision", NEW."founded_year", NEW."headquarters_location", NEW."id", NEW."is_test_data", NEW."location_id", NEW."name", NEW."person_type", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rides_count", NEW."roles", NEW."slug", NEW."source_url", NEW."updated_at", NEW."url", NEW."website"); RETURN NULL;', hash='211e480aa3391c67288564ec1fdfa2552956bbba', operation='UPDATE', pgid='pgtrigger_update_update_456a8', table='rides_company', when='AFTER')), + ), + ] diff --git a/backend/apps/rides/models/company.py b/backend/apps/rides/models/company.py index 5d29094c..4548f486 100644 --- a/backend/apps/rides/models/company.py +++ b/backend/apps/rides/models/company.py @@ -22,9 +22,70 @@ class Company(TrackedModel): ) description = models.TextField(blank=True, help_text="Detailed company description") website = models.URLField(blank=True, help_text="Company website URL") + + # Person/Entity type + PERSON_TYPE_CHOICES = [ + ("company", "Company"), + ("individual", "Individual"), + ("firm", "Firm"), + ("organization", "Organization"), + ] + person_type = models.CharField( + max_length=20, + choices=PERSON_TYPE_CHOICES, + blank=True, + default="company", + help_text="Type of entity (company, individual, firm, organization)", + ) # General company info founded_date = models.DateField(null=True, blank=True, help_text="Date the company was founded") + founded_date_precision = models.CharField( + max_length=20, + choices=[ + ("exact", "Exact"), + ("month", "Month"), + ("year", "Year"), + ("decade", "Decade"), + ("century", "Century"), + ("approximate", "Approximate"), + ], + blank=True, + default="", + help_text="Precision of the founded date", + ) + founded_year = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Year the company was founded (alternative to founded_date)", + ) + headquarters_location = models.CharField( + max_length=200, + blank=True, + help_text="Headquarters location description (e.g., 'Los Angeles, CA, USA')", + ) + + # Location relationship (optional) + location = models.ForeignKey( + "parks.ParkLocation", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="companies", + help_text="Linked location record for headquarters", + ) + + # Image settings - stored as Cloudflare image IDs/URLs + banner_image_id = models.CharField( + max_length=255, + blank=True, + help_text="Cloudflare image ID for banner image", + ) + card_image_id = models.CharField( + max_length=255, + blank=True, + help_text="Cloudflare image ID for card image", + ) # Manufacturer-specific fields rides_count = models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)") @@ -33,6 +94,16 @@ class Company(TrackedModel): # Frontend URL url = models.URLField(blank=True, help_text="Frontend URL for this company") + # Submission metadata fields (from frontend schema) + source_url = models.URLField( + blank=True, + help_text="Source URL for the data (e.g., official website, Wikipedia)", + ) + is_test_data = models.BooleanField( + default=False, + help_text="Whether this is test/development data", + ) + def __str__(self): return self.name diff --git a/backend/apps/rides/models/rides.py b/backend/apps/rides/models/rides.py index 32667e18..a661c96e 100644 --- a/backend/apps/rides/models/rides.py +++ b/backend/apps/rides/models/rides.py @@ -2,6 +2,7 @@ import contextlib import pghistory from django.contrib.auth.models import AbstractBaseUser +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models from django.utils.text import slugify @@ -44,6 +45,11 @@ class RideModel(TrackedModel): blank=True, help_text="Primary category classification", ) + ride_type = models.CharField( + max_length=100, + blank=True, + help_text="Specific ride type within the category (e.g., 'Flying Coaster', 'Inverted Coaster')", + ) # Technical specifications typical_height_range_min_ft = models.DecimalField( @@ -155,6 +161,16 @@ class RideModel(TrackedModel): # Frontend URL url = models.URLField(blank=True, help_text="Frontend URL for this ride model") + # Submission metadata fields (from frontend schema) + source_url = models.URLField( + blank=True, + help_text="Source URL for the data (e.g., manufacturer website)", + ) + is_test_data = models.BooleanField( + default=False, + help_text="Whether this is test/development data", + ) + class Meta(TrackedModel.Meta): verbose_name = "Ride Model" verbose_name_plural = "Ride Models" @@ -509,17 +525,31 @@ class Ride(StateMachineMixin, TrackedModel): ) opening_date = models.DateField(null=True, blank=True) opening_date_precision = models.CharField( - max_length=10, - choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")], - default="DAY", + max_length=20, + choices=[ + ("exact", "Exact Date"), + ("month", "Month and Year"), + ("year", "Year Only"), + ("decade", "Decade"), + ("century", "Century"), + ("approximate", "Approximate"), + ], + default="exact", blank=True, help_text="Precision of the opening date", ) closing_date = models.DateField(null=True, blank=True) closing_date_precision = models.CharField( - max_length=10, - choices=[("YEAR", "Year"), ("MONTH", "Month"), ("DAY", "Day")], - default="DAY", + max_length=20, + choices=[ + ("exact", "Exact Date"), + ("month", "Month and Year"), + ("year", "Year Only"), + ("decade", "Decade"), + ("century", "Century"), + ("approximate", "Approximate"), + ], + default="exact", blank=True, help_text="Precision of the closing date", ) @@ -541,11 +571,268 @@ class Ride(StateMachineMixin, TrackedModel): blank=True, help_text="Minimum age requirement in years (if any)", ) + height_requirement_cm = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Minimum height requirement in centimeters", + ) + duration_seconds = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Ride duration in seconds", + ) + inversions_count = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Number of inversions (for coasters)", + ) # Computed fields for hybrid filtering opening_year = models.IntegerField(null=True, blank=True, db_index=True) search_text = models.TextField(blank=True, db_index=True) + # ===== CATEGORY-SPECIFIC FIELDS ===== + # These fields support the frontend validation schemas in entityValidationSchemas.ts + # Fields are nullable since they only apply to specific ride categories + + # --- Core Stats (6 fields) --- + max_speed_kmh = models.DecimalField( + max_digits=6, + decimal_places=2, + null=True, + blank=True, + help_text="Maximum speed in kilometers per hour", + ) + height_meters = models.DecimalField( + max_digits=6, + decimal_places=2, + null=True, + blank=True, + help_text="Height of the ride structure in meters", + ) + length_meters = models.DecimalField( + max_digits=8, + decimal_places=2, + null=True, + blank=True, + help_text="Total track/ride length in meters", + ) + drop_meters = models.DecimalField( + max_digits=6, + decimal_places=2, + null=True, + blank=True, + help_text="Maximum drop height in meters", + ) + gforce_max = models.DecimalField( + max_digits=4, + decimal_places=2, + null=True, + blank=True, + help_text="Maximum G-force experienced", + ) + intensity_level = models.CharField( + max_length=20, + blank=True, + help_text="Intensity classification: family, thrill, or extreme", + ) + + # --- Coaster-Specific (5 fields) --- + coaster_type = models.CharField( + max_length=20, + blank=True, + help_text="Coaster structure type: steel, wood, or hybrid", + ) + seating_type = models.CharField( + max_length=20, + blank=True, + help_text="Seating configuration: sit_down, inverted, flying, stand_up, etc.", + ) + track_material = ArrayField( + models.CharField(max_length=50), + blank=True, + default=list, + help_text="Track material types (e.g., ['steel', 'wood'])", + ) + support_material = ArrayField( + models.CharField(max_length=50), + blank=True, + default=list, + help_text="Support structure material types", + ) + propulsion_method = ArrayField( + models.CharField(max_length=50), + blank=True, + default=list, + help_text="Propulsion methods (e.g., ['chain_lift', 'lsm'])", + ) + + # --- Water Ride (5 fields) --- + water_depth_cm = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Water depth in centimeters", + ) + splash_height_meters = models.DecimalField( + max_digits=5, + decimal_places=2, + null=True, + blank=True, + help_text="Maximum splash height in meters", + ) + wetness_level = models.CharField( + max_length=20, + blank=True, + help_text="Expected wetness: dry, light, moderate, or soaked", + ) + flume_type = models.CharField( + max_length=100, + blank=True, + help_text="Type of flume or water channel", + ) + boat_capacity = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Number of passengers per boat/vehicle", + ) + + # --- Dark Ride (7 fields) --- + theme_name = models.CharField( + max_length=200, + blank=True, + help_text="Primary theme or IP name", + ) + story_description = models.TextField( + blank=True, + help_text="Narrative or story description for the ride", + ) + show_duration_seconds = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Duration of show elements in seconds", + ) + animatronics_count = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Number of animatronic figures", + ) + projection_type = models.CharField( + max_length=100, + blank=True, + help_text="Type of projection technology used", + ) + ride_system = models.CharField( + max_length=100, + blank=True, + help_text="Ride system type (e.g., trackless, omnimover)", + ) + scenes_count = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Number of distinct scenes or show sections", + ) + + # --- Flat Ride (7 fields) --- + rotation_type = models.CharField( + max_length=20, + blank=True, + help_text="Rotation axis: horizontal, vertical, multi_axis, pendulum, or none", + ) + motion_pattern = models.CharField( + max_length=200, + blank=True, + help_text="Description of the ride's motion pattern", + ) + platform_count = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Number of ride platforms or gondolas", + ) + swing_angle_degrees = models.DecimalField( + max_digits=5, + decimal_places=2, + null=True, + blank=True, + help_text="Maximum swing angle in degrees", + ) + rotation_speed_rpm = models.DecimalField( + max_digits=6, + decimal_places=2, + null=True, + blank=True, + help_text="Rotation speed in revolutions per minute", + ) + arm_length_meters = models.DecimalField( + max_digits=5, + decimal_places=2, + null=True, + blank=True, + help_text="Length of ride arm in meters", + ) + max_height_reached_meters = models.DecimalField( + max_digits=6, + decimal_places=2, + null=True, + blank=True, + help_text="Maximum height reached during ride cycle in meters", + ) + + # --- Kiddie Ride (4 fields) --- + min_age = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Minimum recommended age in years", + ) + max_age = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Maximum recommended age in years", + ) + educational_theme = models.CharField( + max_length=200, + blank=True, + help_text="Educational or learning theme if applicable", + ) + character_theme = models.CharField( + max_length=200, + blank=True, + help_text="Character or IP theme (e.g., Paw Patrol, Sesame Street)", + ) + + # --- Transportation (6 fields) --- + transport_type = models.CharField( + max_length=20, + blank=True, + help_text="Transport mode: train, monorail, skylift, ferry, peoplemover, or cable_car", + ) + route_length_meters = models.DecimalField( + max_digits=8, + decimal_places=2, + null=True, + blank=True, + help_text="Total route length in meters", + ) + stations_count = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Number of stations or stops", + ) + vehicle_capacity = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Passenger capacity per vehicle", + ) + vehicles_count = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Number of vehicles in operation", + ) + round_trip_duration_seconds = models.PositiveIntegerField( + null=True, + blank=True, + help_text="Duration of a complete round trip in seconds", + ) + # Image settings - references to existing photos banner_image = models.ForeignKey( "RidePhoto", @@ -568,6 +855,16 @@ class Ride(StateMachineMixin, TrackedModel): 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") + # Submission metadata fields (from frontend schema) + source_url = models.URLField( + blank=True, + help_text="Source URL for the data (e.g., official website, RCDB)", + ) + is_test_data = models.BooleanField( + default=False, + help_text="Whether this is test/development data", + ) + class Meta(TrackedModel.Meta): verbose_name = "Ride" verbose_name_plural = "Rides" diff --git a/backend/apps/rides/signals.py b/backend/apps/rides/signals.py index cafb5d48..8870bbf9 100644 --- a/backend/apps/rides/signals.py +++ b/backend/apps/rides/signals.py @@ -209,7 +209,7 @@ def update_ride_search_text_on_park_change(sender, instance, **kwargs): logger.exception(f"Failed to update ride search_text on park change: {e}") -@receiver(post_save, sender="parks.Company") +@receiver(post_save, sender="rides.Company") def update_ride_search_text_on_company_change(sender, instance, **kwargs): """ Update ride search_text when manufacturer/designer name changes. diff --git a/backend/apps/rides/tests/test_ride_workflows.py b/backend/apps/rides/tests/test_ride_workflows.py index dc2f9648..6796fec2 100644 --- a/backend/apps/rides/tests/test_ride_workflows.py +++ b/backend/apps/rides/tests/test_ride_workflows.py @@ -30,14 +30,14 @@ class RideOpeningWorkflowTests(TestCase): def _create_ride(self, status="OPERATING", **kwargs): """Helper to create a ride with park.""" - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - # Create manufacturer - manufacturer = Company.objects.create(name=f"Manufacturer {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + # Create manufacturer (from rides.Company) + manufacturer = RideCompany.objects.create(name=f"Manufacturer {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - # Create park with operator - operator = Company.objects.create(name=f"Operator {timezone.now().timestamp()}", roles=["OPERATOR"]) + # Create park with operator (from parks.Company) + operator = ParkCompany.objects.create(name=f"Operator {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Test Park {timezone.now().timestamp()}", slug=f"test-park-{timezone.now().timestamp()}", @@ -84,11 +84,11 @@ class RideMaintenanceWorkflowTests(TestCase): ) def _create_ride(self, status="OPERATING", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr Maint {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op Maint {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr Maint {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op Maint {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park Maint {timezone.now().timestamp()}", slug=f"park-maint-{timezone.now().timestamp()}", @@ -140,11 +140,11 @@ class RideSBNOWorkflowTests(TestCase): ) def _create_ride(self, status="OPERATING", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr SBNO {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op SBNO {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr SBNO {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op SBNO {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park SBNO {timezone.now().timestamp()}", slug=f"park-sbno-{timezone.now().timestamp()}", @@ -234,11 +234,11 @@ class RideScheduledClosureWorkflowTests(TestCase): ) def _create_ride(self, status="OPERATING", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr Closing {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op Closing {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr Closing {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op Closing {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park Closing {timezone.now().timestamp()}", slug=f"park-closing-{timezone.now().timestamp()}", @@ -324,11 +324,11 @@ class RideDemolitionWorkflowTests(TestCase): ) def _create_ride(self, status="CLOSED_PERM", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr Demo {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op Demo {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr Demo {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op Demo {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park Demo {timezone.now().timestamp()}", slug=f"park-demo-{timezone.now().timestamp()}", @@ -383,11 +383,11 @@ class RideRelocationWorkflowTests(TestCase): ) def _create_ride(self, status="CLOSED_PERM", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr Reloc {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op Reloc {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr Reloc {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op Reloc {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park Reloc {timezone.now().timestamp()}", slug=f"park-reloc-{timezone.now().timestamp()}", @@ -445,11 +445,11 @@ class RideWrapperMethodTests(TestCase): ) def _create_ride(self, status="OPERATING", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr Wrapper {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op Wrapper {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr Wrapper {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op Wrapper {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park Wrapper {timezone.now().timestamp()}", slug=f"park-wrapper-{timezone.now().timestamp()}", @@ -573,11 +573,11 @@ class RidePostClosingStatusAutomationTests(TestCase): ) def _create_ride(self, status="CLOSING", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr Auto {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op Auto {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr Auto {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op Auto {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park Auto {timezone.now().timestamp()}", slug=f"park-auto-{timezone.now().timestamp()}", @@ -659,11 +659,11 @@ class RideStateLogTests(TestCase): ) def _create_ride(self, status="OPERATING", **kwargs): - from apps.parks.models import Company, Park - from apps.rides.models import Ride + from apps.parks.models import Company as ParkCompany, Park + from apps.rides.models import Company as RideCompany, Ride - manufacturer = Company.objects.create(name=f"Mfr Log {timezone.now().timestamp()}", roles=["MANUFACTURER"]) - operator = Company.objects.create(name=f"Op Log {timezone.now().timestamp()}", roles=["OPERATOR"]) + manufacturer = RideCompany.objects.create(name=f"Mfr Log {timezone.now().timestamp()}", roles=["MANUFACTURER"]) + operator = ParkCompany.objects.create(name=f"Op Log {timezone.now().timestamp()}", roles=["OPERATOR"]) park = Park.objects.create( name=f"Park Log {timezone.now().timestamp()}", slug=f"park-log-{timezone.now().timestamp()}", diff --git a/backend/celerybeat-schedule-wal b/backend/celerybeat-schedule-wal index d18d58231e42970a17520d4c83599eeed664a559..13e786f83c2766c27650bb05f833d8d455f58340 100644 GIT binary patch literal 2616232 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