diff --git a/companies/admin.py b/companies/admin.py index ac3be2e9..0b76665f 100644 --- a/companies/admin.py +++ b/companies/admin.py @@ -1,16 +1,15 @@ from django.contrib import admin -from simple_history.admin import SimpleHistoryAdmin from .models import Company, Manufacturer @admin.register(Company) -class CompanyAdmin(SimpleHistoryAdmin): +class CompanyAdmin(admin.ModelAdmin): list_display = ('id', 'name', 'headquarters', 'website', 'created_at') search_fields = ('name', 'headquarters', 'description') prepopulated_fields = {'slug': ('name',)} readonly_fields = ('created_at', 'updated_at') @admin.register(Manufacturer) -class ManufacturerAdmin(SimpleHistoryAdmin): +class ManufacturerAdmin(admin.ModelAdmin): list_display = ('id', 'name', 'headquarters', 'website', 'created_at') search_fields = ('name', 'headquarters', 'description') prepopulated_fields = {'slug': ('name',)} diff --git a/companies/migrations/0003_companyevent_manufacturerevent.py b/companies/migrations/0003_companyevent_manufacturerevent.py new file mode 100644 index 00000000..e0340447 --- /dev/null +++ b/companies/migrations/0003_companyevent_manufacturerevent.py @@ -0,0 +1,55 @@ +# Generated by Django 5.1.4 on 2025-02-09 15:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("companies", "0002_add_designer_model"), + ] + + operations = [ + migrations.CreateModel( + name="CompanyEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("website", models.URLField(blank=True)), + ("headquarters", models.CharField(blank=True, max_length=255)), + ("description", models.TextField(blank=True)), + ("total_parks", models.IntegerField(default=0)), + ("total_rides", models.IntegerField(default=0)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ManufacturerEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("website", models.URLField(blank=True)), + ("headquarters", models.CharField(blank=True, max_length=255)), + ("description", models.TextField(blank=True)), + ("total_rides", models.IntegerField(default=0)), + ("total_roller_coasters", models.IntegerField(default=0)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/companies/migrations/0004_delete_designer_alter_company_id_and_more.py b/companies/migrations/0004_delete_designer_alter_company_id_and_more.py new file mode 100644 index 00000000..94c2b7b0 --- /dev/null +++ b/companies/migrations/0004_delete_designer_alter_company_id_and_more.py @@ -0,0 +1,131 @@ +# Generated by Django 5.1.4 on 2025-02-09 15:31 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("companies", "0003_companyevent_manufacturerevent"), + ("pghistory", "0006_delete_aggregateevent"), + ("rides", "0010_rideevent_ridemodelevent_and_more"), + ] + + operations = [ + migrations.DeleteModel( + name="Designer", + ), + migrations.AlterField( + model_name="company", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name="manufacturer", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + ), + pgtrigger.migrations.AddTrigger( + model_name="company", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "companies_companyevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_parks", "total_rides", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."total_parks", NEW."total_rides", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="413671b13a748fb5f1acd57e8ec4af12ad7ae215", + operation="INSERT", + pgid="pgtrigger_insert_insert_a4101", + table="companies_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 "companies_companyevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_parks", "total_rides", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."total_parks", NEW."total_rides", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="ee3eff1c96e46769347b8463d527668b7ece63c4", + operation="UPDATE", + pgid="pgtrigger_update_update_3d5ae", + table="companies_company", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="manufacturer", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "companies_manufacturerevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_rides", "total_roller_coasters", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."total_rides", NEW."total_roller_coasters", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="ac3c4c31aa8dffe569154454a6c4479d189c0f64", + operation="INSERT", + pgid="pgtrigger_insert_insert_5c0b6", + table="companies_manufacturer", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="manufacturer", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "companies_manufacturerevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_rides", "total_roller_coasters", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."total_rides", NEW."total_roller_coasters", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="c46f36f5811cd843ff61eab3ae77624ae2e69f60", + operation="UPDATE", + pgid="pgtrigger_update_update_81971", + table="companies_manufacturer", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="companyevent", + name="pgh_context", + field=models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="pghistory.context", + ), + ), + migrations.AddField( + model_name="companyevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="companies.company", + ), + ), + migrations.AddField( + model_name="manufacturerevent", + 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="manufacturerevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="companies.manufacturer", + ), + ), + ] diff --git a/companies/models.py b/companies/models.py index 240c7bf1..f48a5cf6 100644 --- a/companies/models.py +++ b/companies/models.py @@ -2,11 +2,11 @@ from django.db import models from django.utils.text import slugify from django.urls import reverse from typing import Tuple, Optional, ClassVar, TYPE_CHECKING +import pghistory +from history_tracking.models import TrackedModel, HistoricalSlug -if TYPE_CHECKING: - from history_tracking.models import HistoricalSlug - -class Company(models.Model): +@pghistory.track() +class Company(TrackedModel): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255, unique=True) website = models.URLField(blank=True) @@ -37,8 +37,18 @@ class Company(models.Model): try: return cls.objects.get(slug=slug), False except cls.DoesNotExist: - # Check historical slugs - from history_tracking.models import HistoricalSlug + # Check pghistory first + history_model = cls.get_history_model() + history_entry = ( + history_model.objects.filter(slug=slug) + .order_by('-pgh_created_at') + .first() + ) + + if history_entry: + return cls.objects.get(id=history_entry.pgh_obj_id), True + + # Check manual slug history as fallback try: historical = HistoricalSlug.objects.get( content_type__model='company', @@ -48,7 +58,8 @@ class Company(models.Model): except (HistoricalSlug.DoesNotExist, cls.DoesNotExist): raise cls.DoesNotExist() -class Manufacturer(models.Model): +@pghistory.track() +class Manufacturer(TrackedModel): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255, unique=True) website = models.URLField(blank=True) @@ -78,8 +89,18 @@ class Manufacturer(models.Model): try: return cls.objects.get(slug=slug), False except cls.DoesNotExist: - # Check historical slugs - from history_tracking.models import HistoricalSlug + # Check pghistory first + history_model = cls.get_history_model() + history_entry = ( + history_model.objects.filter(slug=slug) + .order_by('-pgh_created_at') + .first() + ) + + if history_entry: + return cls.objects.get(id=history_entry.pgh_obj_id), True + + # Check manual slug history as fallback try: historical = HistoricalSlug.objects.get( content_type__model='manufacturer', @@ -88,43 +109,3 @@ class Manufacturer(models.Model): return cls.objects.get(pk=historical.object_id), True except (HistoricalSlug.DoesNotExist, cls.DoesNotExist): raise cls.DoesNotExist() - -class Designer(models.Model): - name = models.CharField(max_length=255) - slug = models.SlugField(max_length=255, unique=True) - website = models.URLField(blank=True) - description = models.TextField(blank=True) - total_rides = models.IntegerField(default=0) - total_roller_coasters = models.IntegerField(default=0) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - objects: ClassVar[models.Manager['Designer']] - - class Meta: - ordering = ['name'] - - def __str__(self) -> str: - return self.name - - def save(self, *args, **kwargs) -> None: - if not self.slug: - self.slug = slugify(self.name) - super().save(*args, **kwargs) - - @classmethod - def get_by_slug(cls, slug: str) -> Tuple['Designer', bool]: - """Get designer by slug, checking historical slugs if needed""" - try: - return cls.objects.get(slug=slug), False - except cls.DoesNotExist: - # Check historical slugs - from history_tracking.models import HistoricalSlug - try: - historical = HistoricalSlug.objects.get( - content_type__model='designer', - slug=slug - ) - return cls.objects.get(pk=historical.object_id), True - except (HistoricalSlug.DoesNotExist, cls.DoesNotExist): - raise cls.DoesNotExist() diff --git a/core/models.py b/core/models.py index a717d3e2..015adb7f 100644 --- a/core/models.py +++ b/core/models.py @@ -2,18 +2,7 @@ from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.utils.text import slugify -import pghistory - -@pghistory.track() -class HistoricalModel(models.Model): - """ - Abstract base model that provides universal history tracking via django-pghistory. - """ - class Meta: - abstract = True - - def save(self, *args, **kwargs): - return super().save(*args, **kwargs) +from history_tracking.models import TrackedModel class SlugHistory(models.Model): """ @@ -38,11 +27,9 @@ class SlugHistory(models.Model): def __str__(self): return f"Old slug '{self.old_slug}' for {self.content_object}" -@pghistory.track() -class SluggedModel(HistoricalModel): +class SluggedModel(TrackedModel): """ Abstract base model that provides slug functionality with history tracking. - Inherits from HistoricalModel to get universal history tracking. """ name = models.CharField(max_length=200) slug = models.SlugField(max_length=200, unique=True) @@ -69,7 +56,6 @@ class SluggedModel(HistoricalModel): if not self.slug: self.slug = slugify(self.name) - # Call HistoricalModel's save to ensure history tracking super().save(*args, **kwargs) def get_id_field_name(self): @@ -91,7 +77,18 @@ class SluggedModel(HistoricalModel): # Try to get by current slug first return cls.objects.get(slug=slug), False except cls.DoesNotExist: - # Try to find in slug history + # Check pghistory first + history_model = cls.get_history_model() + history_entry = ( + history_model.objects.filter(slug=slug) + .order_by('-pgh_created_at') + .first() + ) + + if history_entry: + return cls.objects.get(id=history_entry.pgh_obj_id), True + + # Try to find in manual slug history as fallback history = SlugHistory.objects.filter( content_type=ContentType.objects.get_for_model(cls), old_slug=slug diff --git a/designers/admin.py b/designers/admin.py index 26f980e0..4f4e9ef5 100644 --- a/designers/admin.py +++ b/designers/admin.py @@ -1,10 +1,13 @@ from django.contrib import admin -from simple_history.admin import SimpleHistoryAdmin +from django.utils.text import slugify from .models import Designer @admin.register(Designer) -class DesignerAdmin(SimpleHistoryAdmin): +class DesignerAdmin(admin.ModelAdmin): list_display = ('name', 'headquarters', 'founded_date', 'website') search_fields = ('name', 'headquarters') - list_filter = ('founded_date',) prepopulated_fields = {'slug': ('name',)} + readonly_fields = ('created_at', 'updated_at') + + def get_queryset(self, request): + return super().get_queryset(request).select_related() diff --git a/designers/migrations/0002_designerevent_remove_historicaldesigner_history_user_and_more.py b/designers/migrations/0002_designerevent_remove_historicaldesigner_history_user_and_more.py new file mode 100644 index 00000000..5a20d891 --- /dev/null +++ b/designers/migrations/0002_designerevent_remove_historicaldesigner_history_user_and_more.py @@ -0,0 +1,99 @@ +# Generated by Django 5.1.4 on 2025-02-09 15:24 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("designers", "0001_initial"), + ("pghistory", "0006_delete_aggregateevent"), + ] + + operations = [ + migrations.CreateModel( + name="DesignerEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("description", models.TextField(blank=True)), + ("website", models.URLField(blank=True)), + ("founded_date", models.DateField(blank=True, null=True)), + ("headquarters", models.CharField(blank=True, max_length=255)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.RemoveField( + model_name="historicaldesigner", + name="history_user", + ), + migrations.AlterField( + model_name="designer", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + ), + pgtrigger.migrations.AddTrigger( + model_name="designer", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "designers_designerevent" ("created_at", "description", "founded_date", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_date", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="876eaa3e1c7cf234f03cc706fa4e5e508ed780db", + operation="INSERT", + pgid="pgtrigger_insert_insert_9be65", + table="designers_designer", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="designer", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "designers_designerevent" ("created_at", "description", "founded_date", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_date", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="edb092b6a122ca5827740a9afcdc6a885fe69c1c", + operation="UPDATE", + pgid="pgtrigger_update_update_b5f91", + table="designers_designer", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="designerevent", + 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="designerevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="designers.designer", + ), + ), + migrations.DeleteModel( + name="HistoricalDesigner", + ), + ] diff --git a/designers/models.py b/designers/models.py index 912503c4..23351255 100644 --- a/designers/models.py +++ b/designers/models.py @@ -1,8 +1,10 @@ from django.db import models from django.utils.text import slugify -from simple_history.models import HistoricalRecords +from history_tracking.models import TrackedModel +import pghistory -class Designer(models.Model): +@pghistory.track() +class Designer(TrackedModel): name = models.CharField(max_length=255) slug = models.SlugField(max_length=255, unique=True) description = models.TextField(blank=True) @@ -11,7 +13,6 @@ class Designer(models.Model): headquarters = models.CharField(max_length=255, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - history = HistoricalRecords() class Meta: ordering = ['name'] @@ -30,8 +31,13 @@ class Designer(models.Model): try: return cls.objects.get(slug=slug), False except cls.DoesNotExist: - # Check historical slugs - history = cls.history.filter(slug=slug).order_by('-history_date').first() + # Check historical slugs using pghistory + history_model = cls.get_history_model() + history = ( + history_model.objects.filter(slug=slug) + .order_by('-pgh_created_at') + .first() + ) if history: - return cls.objects.get(id=history.id), True + return cls.objects.get(id=history.pgh_obj_id), True raise cls.DoesNotExist("No designer found with this slug") diff --git a/history_tracking/apps.py b/history_tracking/apps.py index f7f856e2..7bcc9c91 100644 --- a/history_tracking/apps.py +++ b/history_tracking/apps.py @@ -7,20 +7,9 @@ class HistoryTrackingConfig(AppConfig): name = "history_tracking" def ready(self): - from django.apps import apps - from .mixins import HistoricalChangeMixin - - # Get the Park model - try: - Park = apps.get_model('parks', 'Park') - ParkArea = apps.get_model('parks', 'ParkArea') - - # Apply mixin to historical models - if HistoricalChangeMixin not in Park.history.model.__bases__: - Park.history.model.__bases__ = (HistoricalChangeMixin,) + Park.history.model.__bases__ - - if HistoricalChangeMixin not in ParkArea.history.model.__bases__: - ParkArea.history.model.__bases__ = (HistoricalChangeMixin,) + ParkArea.history.model.__bases__ - except LookupError: - # Models might not be loaded yet - pass + """ + No initialization needed for pghistory tracking. + History tracking is handled by the @pghistory.track() decorator + and triggers installed in migrations. + """ + pass diff --git a/history_tracking/models.py b/history_tracking/models.py index 234fd852..985dce4d 100644 --- a/history_tracking/models.py +++ b/history_tracking/models.py @@ -2,6 +2,7 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.auth import get_user_model from simple_history.models import HistoricalRecords from .mixins import HistoricalChangeMixin from typing import Any, Type, TypeVar, cast @@ -9,8 +10,46 @@ from django.db.models import QuerySet T = TypeVar('T', bound=models.Model) +class DiffMixin: + """Mixin to add diffing capabilities to pghistory events""" + def get_prev_record(self): + """Get the previous record for this instance""" + try: + return type(self).objects.filter( + pgh_created_at__lt=self.pgh_created_at, + pgh_obj_id=self.pgh_obj_id + ).order_by('-pgh_created_at').first() + except (AttributeError, TypeError): + return None + + def diff_against_previous(self): + """Compare this record against the previous one""" + prev_record = self.get_prev_record() + if not prev_record: + return {} + + changes = {} + skip_fields = { + 'pgh_id', 'pgh_created_at', 'pgh_label', + 'pgh_obj_id', 'pgh_context_id', '_state' + } + + for field in self.__dict__: + if field not in skip_fields and not field.startswith('_'): + try: + old_value = getattr(prev_record, field) + new_value = getattr(self, field) + if old_value != new_value: + changes[field] = {"old": str(old_value), "new": str(new_value)} + except AttributeError: + continue + return changes + class HistoricalModel(models.Model): - """Abstract base class for models with history tracking""" + """ + Legacy abstract base class for models with history tracking. + Use TrackedModel for new implementations. + """ id = models.BigAutoField(primary_key=True) history: HistoricalRecords = HistoricalRecords( inherit=True, @@ -30,6 +69,27 @@ class HistoricalModel(models.Model): model = self._history_model return model.objects.filter(id=self.pk).order_by('-history_date') +class TrackedModel(models.Model): + """Abstract base class for models with pghistory tracking. + The @pghistory.track() decorator should be applied to concrete models + that inherit from this class. + """ + id = models.BigAutoField(primary_key=True) + + class Meta: + abstract = True + + def get_history(self) -> QuerySet: + """Get all history records for this instance in chronological order""" + history_model = self.get_history_model() + return history_model.objects.filter( + pgh_obj_id=self.pk + ).order_by('-pgh_created_at') + + def get_history_model(self) -> Type[Any]: + """Get the pghistory model for this instance""" + return self.pgh_obj_event_model + class HistoricalSlug(models.Model): """Track historical slugs for models""" content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) diff --git a/location/migrations/0002_locationevent_remove_historicallocation_content_type_and_more.py b/location/migrations/0002_locationevent_remove_historicallocation_content_type_and_more.py new file mode 100644 index 00000000..b0a75bd4 --- /dev/null +++ b/location/migrations/0002_locationevent_remove_historicallocation_content_type_and_more.py @@ -0,0 +1,179 @@ +# Generated by Django 5.1.4 on 2025-02-09 16:13 + +import django.contrib.gis.db.models.fields +import django.core.validators +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("location", "0001_initial"), + ("pghistory", "0006_delete_aggregateevent"), + ] + + operations = [ + migrations.CreateModel( + name="LocationEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("object_id", models.PositiveIntegerField()), + ( + "name", + models.CharField( + help_text="Name of the location (e.g. business name, landmark)", + max_length=255, + ), + ), + ( + "location_type", + models.CharField( + help_text="Type of location (e.g. business, landmark, address)", + max_length=50, + ), + ), + ( + "latitude", + models.DecimalField( + blank=True, + decimal_places=6, + help_text="Latitude coordinate (legacy field)", + max_digits=9, + null=True, + validators=[ + django.core.validators.MinValueValidator(-90), + django.core.validators.MaxValueValidator(90), + ], + ), + ), + ( + "longitude", + models.DecimalField( + blank=True, + decimal_places=6, + help_text="Longitude coordinate (legacy field)", + max_digits=9, + null=True, + validators=[ + django.core.validators.MinValueValidator(-180), + django.core.validators.MaxValueValidator(180), + ], + ), + ), + ( + "point", + django.contrib.gis.db.models.fields.PointField( + blank=True, + help_text="Geographic coordinates as a Point", + null=True, + srid=4326, + ), + ), + ( + "street_address", + models.CharField(blank=True, max_length=255, null=True), + ), + ("city", models.CharField(blank=True, max_length=100, null=True)), + ( + "state", + models.CharField( + blank=True, + help_text="State/Region/Province", + max_length=100, + null=True, + ), + ), + ("country", models.CharField(blank=True, max_length=100, null=True)), + ("postal_code", models.CharField(blank=True, max_length=20, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.RemoveField( + model_name="historicallocation", + name="content_type", + ), + migrations.RemoveField( + model_name="historicallocation", + name="history_user", + ), + migrations.AlterField( + model_name="location", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + ), + pgtrigger.migrations.AddTrigger( + model_name="location", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "location_locationevent" ("city", "content_type_id", "country", "created_at", "id", "latitude", "location_type", "longitude", "name", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "state", "street_address", "updated_at") VALUES (NEW."city", NEW."content_type_id", NEW."country", NEW."created_at", NEW."id", NEW."latitude", NEW."location_type", NEW."longitude", NEW."name", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."point", NEW."postal_code", NEW."state", NEW."street_address", NEW."updated_at"); RETURN NULL;', + hash="8a8f00869cfcaa1a23ab29b3d855e83602172c67", + operation="INSERT", + pgid="pgtrigger_insert_insert_98cd4", + table="location_location", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="location", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "location_locationevent" ("city", "content_type_id", "country", "created_at", "id", "latitude", "location_type", "longitude", "name", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "state", "street_address", "updated_at") VALUES (NEW."city", NEW."content_type_id", NEW."country", NEW."created_at", NEW."id", NEW."latitude", NEW."location_type", NEW."longitude", NEW."name", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."point", NEW."postal_code", NEW."state", NEW."street_address", NEW."updated_at"); RETURN NULL;', + hash="f3378cb26a5d88aa82c8fae016d46037b530de90", + operation="UPDATE", + pgid="pgtrigger_update_update_471d2", + table="location_location", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="locationevent", + name="content_type", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="contenttypes.contenttype", + ), + ), + migrations.AddField( + model_name="locationevent", + 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="locationevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="location.location", + ), + ), + migrations.DeleteModel( + name="HistoricalLocation", + ), + ] diff --git a/location/models.py b/location/models.py index b758aa78..3ee2c79e 100644 --- a/location/models.py +++ b/location/models.py @@ -3,10 +3,12 @@ from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.validators import MinValueValidator, MaxValueValidator -from simple_history.models import HistoricalRecords from django.contrib.gis.geos import Point +import pghistory +from history_tracking.models import TrackedModel -class Location(models.Model): +@pghistory.track() +class Location(TrackedModel): """ A generic location model that can be associated with any model using GenericForeignKey. Stores detailed location information @@ -63,7 +65,6 @@ class Location(models.Model): # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - history = HistoricalRecords() class Meta: indexes = [ diff --git a/memory-bank/decisions/history-tracking-migration.md b/memory-bank/decisions/history-tracking-migration.md new file mode 100644 index 00000000..07ad6351 --- /dev/null +++ b/memory-bank/decisions/history-tracking-migration.md @@ -0,0 +1,90 @@ +# History Tracking Migration + +## Context +The project is transitioning from django-simple-history to django-pghistory for model history tracking. + +## Implementation Details + +### Base Implementation (history_tracking/models.py) +- Both old and new implementations maintained during transition: + - `HistoricalModel` - Legacy base class using django-simple-history + - `TrackedModel` - New base class using django-pghistory +- Custom `DiffMixin` for comparing historical records +- Maintained `HistoricalSlug` for backward compatibility + +### Transition Strategy +1. Maintain Backward Compatibility + - Keep both HistoricalModel and TrackedModel during transition + - Update models one at a time to use TrackedModel + - Ensure no breaking changes during migration + +2. Model Updates + - Designer (Completed) + - Migrated to TrackedModel + - Updated get_by_slug to use pghistory queries + - Removed SimpleHistoryAdmin dependency + + - Pending Model Updates + - Companies (Company, Manufacturer) + - Parks (Park, ParkArea) + - Rides (Ride, RollerCoasterStats) + - Location models + +### Migration Process +1. For Each Model: + - Switch base class from HistoricalModel to TrackedModel + - Update admin.py to remove SimpleHistoryAdmin + - Create and apply migrations + - Test history tracking functionality + - Update any history-related queries + +2. Testing Steps + - Create test objects + - Make changes + - Verify history records + - Check diff functionality + - Validate historical slug lookup + +3. Admin Integration + - Remove SimpleHistoryAdmin + - Use standard ModelAdmin + - Keep existing list displays and search fields + +## Benefits +- Native PostgreSQL trigger-based tracking +- More efficient storage and querying +- Better performance characteristics +- Context tracking capabilities + +## Rollback Plan +Since both implementations are maintained: +1. Revert model inheritance to HistoricalModel +2. Restore SimpleHistoryAdmin +3. Keep existing migrations + +## Next Steps +1. Create migrations for Designer model +2. Update remaining models in this order: + a. Companies app + b. Parks app + c. Rides app + d. Location app +3. Test historical functionality +4. Once all models are migrated: + - Remove HistoricalModel class + - Remove django-simple-history dependency + - Update documentation + +## Technical Notes +- Uses pghistory's default tracking configuration +- Maintains compatibility with existing code patterns +- Custom diff functionality preserved +- Historical slug tracking unchanged +- Both tracking systems can coexist during migration + +## Completion Criteria +1. All models migrated to TrackedModel +2. All functionality tested and working +3. No dependencies on django-simple-history +4. Documentation updated to reflect new implementation +5. All migrations applied successfully \ No newline at end of file diff --git a/memory-bank/decisions/migration-progress.md b/memory-bank/decisions/migration-progress.md new file mode 100644 index 00000000..50e834f0 --- /dev/null +++ b/memory-bank/decisions/migration-progress.md @@ -0,0 +1,98 @@ +# PGHistory Migration Progress + +## All Migrations Complete! 🎉 + +### Latest Migration +- `location/migrations/0002_locationevent_remove_historicallocation_content_type_and_more.py` + - Created LocationEvent model + - Removed simple-history fields + - Set up pghistory triggers + - Cleaned up historical models + +### Previously Completed Migrations + +1. Companies App + - Created CompanyEvent and ManufacturerEvent + - Removed Designer model + - Set up pghistory triggers + +2. Rides App + - Created RideEvent and RideModelEvent + - Removed simple-history fields + - Updated Designer foreign key + - Set up pghistory triggers + +3. Parks App + - Created ParkEvent and ParkAreaEvent models + - Set up pghistory tracking triggers + - Removed simple-history fields and models + +4. Designers App + - Created DesignerEvent model + - Set up insert/update triggers + - Full pghistory implementation + +5. Moderation Models + - Created EditSubmissionEvent model + - Created PhotoSubmissionEvent model + - Set up triggers for both models + +## Infrastructure Updates +1. History Tracking App + - Removed simple-history initialization from apps.py + - Updated base models to use pghistory + - Added DiffMixin for tracking changes + +## Final Steps + +### 1. Remove django-simple-history +```bash +# Update requirements.txt +- Remove django-simple-history==3.8.0 +``` + +### 2. Clean Up Configuration +- Remove any remaining simple-history settings +- Update documentation for new history tracking +- Add migration guide for future models + +### 3. Testing +1. Test all models: + - Create/Update/Delete operations + - Historical queries + - Change tracking + - Event context + +2. Verify functionality: + - Slug history lookups + - Model relationships + - Admin interfaces + +### 4. Documentation Updates +1. Update model documentation +2. Add pghistory usage examples +3. Document migration patterns +4. Update contributor guide + +## Technical Notes +- PGHistory tracking implemented via triggers +- Event models store complete history +- Foreign key relationships preserved +- Context tracking available +- GeoDjango fields supported +- Improved query performance expected + +## Migration Statistics +✅ Designer Model +✅ Moderation Models +✅ Companies Models +✅ Rides Models +✅ Parks Models +✅ Location Models + +## Lessons Learned +1. Keep backward compatibility during transition +2. Migrate models in dependency order +3. Test thoroughly after each migration +4. Update related code incrementally +5. Maintain documentation throughout \ No newline at end of file diff --git a/moderation/admin.py b/moderation/admin.py index b789b30a..a0acff97 100644 --- a/moderation/admin.py +++ b/moderation/admin.py @@ -3,7 +3,6 @@ from django.contrib.admin import AdminSite from django.utils.html import format_html from django.urls import reverse from django.utils.safestring import mark_safe -import pghistory from .models import EditSubmission, PhotoSubmission class ModerationAdminSite(AdminSite): @@ -77,7 +76,7 @@ class PhotoSubmissionAdmin(admin.ModelAdmin): obj.reject(request.user, obj.notes) super().save_model(request, obj, form, change) -class HistoryAdmin(admin.ModelAdmin): +class HistoryEventAdmin(admin.ModelAdmin): """Admin interface for viewing model history events""" list_display = ['pgh_label', 'pgh_created_at', 'get_object_link', 'get_context'] list_filter = ['pgh_label', 'pgh_created_at'] @@ -106,4 +105,6 @@ class HistoryAdmin(admin.ModelAdmin): # Register with moderation site only moderation_site.register(EditSubmission, EditSubmissionAdmin) moderation_site.register(PhotoSubmission, PhotoSubmissionAdmin) -moderation_site.register(pghistory.models.Event, HistoryAdmin) + +# We will register concrete event models as they are created during migrations +# Example: moderation_site.register(DesignerEvent, HistoryEventAdmin) diff --git a/moderation/migrations/0005_editsubmissionevent_photosubmissionevent_and_more.py b/moderation/migrations/0005_editsubmissionevent_photosubmissionevent_and_more.py new file mode 100644 index 00000000..6a59c434 --- /dev/null +++ b/moderation/migrations/0005_editsubmissionevent_photosubmissionevent_and_more.py @@ -0,0 +1,305 @@ +# Generated by Django 5.1.4 on 2025-02-09 15:24 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("moderation", "0004_add_moderator_changes"), + ("pghistory", "0006_delete_aggregateevent"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="EditSubmissionEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("object_id", models.PositiveIntegerField(blank=True, null=True)), + ( + "submission_type", + models.CharField( + choices=[("EDIT", "Edit Existing"), ("CREATE", "Create New")], + default="EDIT", + max_length=10, + ), + ), + ( + "changes", + models.JSONField( + help_text="JSON representation of the changes or new object data" + ), + ), + ( + "moderator_changes", + models.JSONField( + blank=True, + help_text="Moderator's edited version of the changes before approval", + null=True, + ), + ), + ( + "reason", + models.TextField(help_text="Why this edit/addition is needed"), + ), + ( + "source", + models.TextField( + blank=True, help_text="Source of information (if applicable)" + ), + ), + ( + "status", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("ESCALATED", "Escalated"), + ], + default="PENDING", + max_length=20, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("handled_at", models.DateTimeField(blank=True, null=True)), + ( + "notes", + models.TextField( + blank=True, + help_text="Notes from the moderator about this submission", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="PhotoSubmissionEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("object_id", models.PositiveIntegerField()), + ("photo", models.ImageField(upload_to="submissions/photos/")), + ("caption", models.CharField(blank=True, max_length=255)), + ("date_taken", models.DateField(blank=True, null=True)), + ( + "status", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("ESCALATED", "Escalated"), + ], + default="PENDING", + max_length=20, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("handled_at", models.DateTimeField(blank=True, null=True)), + ( + "notes", + models.TextField( + blank=True, + help_text="Notes from the moderator about this photo submission", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AlterField( + model_name="editsubmission", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name="photosubmission", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + ), + pgtrigger.migrations.AddTrigger( + model_name="editsubmission", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "moderation_editsubmissionevent" ("changes", "content_type_id", "created_at", "handled_at", "handled_by_id", "id", "moderator_changes", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reason", "source", "status", "submission_type", "user_id") VALUES (NEW."changes", NEW."content_type_id", NEW."created_at", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."moderator_changes", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."reason", NEW."source", NEW."status", NEW."submission_type", NEW."user_id"); RETURN NULL;', + hash="616bbed667e6f8a1b23dfa39b5b3fd0b3bc0b43d", + operation="INSERT", + pgid="pgtrigger_insert_insert_2c796", + table="moderation_editsubmission", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="editsubmission", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "moderation_editsubmissionevent" ("changes", "content_type_id", "created_at", "handled_at", "handled_by_id", "id", "moderator_changes", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reason", "source", "status", "submission_type", "user_id") VALUES (NEW."changes", NEW."content_type_id", NEW."created_at", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."moderator_changes", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."reason", NEW."source", NEW."status", NEW."submission_type", NEW."user_id"); RETURN NULL;', + hash="76c447d8cfeced3bb1893e2d900c97bb05a9f028", + operation="UPDATE", + pgid="pgtrigger_update_update_ab38f", + table="moderation_editsubmission", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="photosubmission", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "moderation_photosubmissionevent" ("caption", "content_type_id", "created_at", "date_taken", "handled_at", "handled_by_id", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo", "status", "user_id") VALUES (NEW."caption", NEW."content_type_id", NEW."created_at", NEW."date_taken", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photo", NEW."status", NEW."user_id"); RETURN NULL;', + hash="ea6563a26e5875de544fa270751df4f48003a4c0", + operation="INSERT", + pgid="pgtrigger_insert_insert_62865", + table="moderation_photosubmission", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="photosubmission", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "moderation_photosubmissionevent" ("caption", "content_type_id", "created_at", "date_taken", "handled_at", "handled_by_id", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo", "status", "user_id") VALUES (NEW."caption", NEW."content_type_id", NEW."created_at", NEW."date_taken", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photo", NEW."status", NEW."user_id"); RETURN NULL;', + hash="64f35eedbad17d4060eaeab7f2bd944620465591", + operation="UPDATE", + pgid="pgtrigger_update_update_9c311", + table="moderation_photosubmission", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="editsubmissionevent", + name="content_type", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="contenttypes.contenttype", + ), + ), + migrations.AddField( + model_name="editsubmissionevent", + name="handled_by", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="editsubmissionevent", + 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="editsubmissionevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="moderation.editsubmission", + ), + ), + migrations.AddField( + model_name="editsubmissionevent", + name="user", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="photosubmissionevent", + name="content_type", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="contenttypes.contenttype", + ), + ), + migrations.AddField( + model_name="photosubmissionevent", + name="handled_by", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="photosubmissionevent", + 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="photosubmissionevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="moderation.photosubmission", + ), + ), + migrations.AddField( + model_name="photosubmissionevent", + name="user", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/moderation/models.py b/moderation/models.py index 2823442c..6d59e73d 100644 --- a/moderation/models.py +++ b/moderation/models.py @@ -10,12 +10,12 @@ from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import AnonymousUser from django.utils.text import slugify import pghistory -from core.models import HistoricalModel +from history_tracking.models import TrackedModel UserType = Union[AbstractBaseUser, AnonymousUser] @pghistory.track() # Track all changes by default -class EditSubmission(HistoricalModel): +class EditSubmission(TrackedModel): STATUS_CHOICES = [ ("PENDING", "Pending"), ("APPROVED", "Approved"), @@ -199,7 +199,7 @@ class EditSubmission(HistoricalModel): self.save() @pghistory.track() # Track all changes by default -class PhotoSubmission(HistoricalModel): +class PhotoSubmission(TrackedModel): STATUS_CHOICES = [ ("PENDING", "Pending"), ("APPROVED", "Approved"), diff --git a/parks/admin.py b/parks/admin.py index 21f73e03..32f4ff04 100644 --- a/parks/admin.py +++ b/parks/admin.py @@ -1,9 +1,8 @@ from django.contrib import admin from django.utils.html import format_html -from simple_history.admin import SimpleHistoryAdmin from .models import Park, ParkArea -class ParkAdmin(SimpleHistoryAdmin): +class ParkAdmin(admin.ModelAdmin): list_display = ('name', 'formatted_location', 'status', 'owner', 'created_at', 'updated_at') list_filter = ('status',) search_fields = ('name', 'description', 'location__name', 'location__city', 'location__country') @@ -15,21 +14,13 @@ class ParkAdmin(SimpleHistoryAdmin): return obj.formatted_location formatted_location.short_description = 'Location' - def get_history_list_display(self, request): - """Customize the list display for history records""" - return ('name', 'formatted_location', 'status', 'history_date', 'history_user') - -class ParkAreaAdmin(SimpleHistoryAdmin): +class ParkAreaAdmin(admin.ModelAdmin): list_display = ('name', 'park', 'created_at', 'updated_at') list_filter = ('park',) search_fields = ('name', 'description', 'park__name') readonly_fields = ('created_at', 'updated_at') prepopulated_fields = {'slug': ('name',)} - def get_history_list_display(self, request): - """Customize the list display for history records""" - return ('name', 'park', 'history_date', 'history_user') - # Register the models with their admin classes admin.site.register(Park, ParkAdmin) admin.site.register(ParkArea, ParkAreaAdmin) diff --git a/parks/migrations/0002_parkareaevent_parkevent_and_more.py b/parks/migrations/0002_parkareaevent_parkevent_and_more.py new file mode 100644 index 00000000..bcebb76b --- /dev/null +++ b/parks/migrations/0002_parkareaevent_parkevent_and_more.py @@ -0,0 +1,233 @@ +# Generated by Django 5.1.4 on 2025-02-09 15:44 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("companies", "0004_delete_designer_alter_company_id_and_more"), + ("parks", "0001_initial"), + ("pghistory", "0006_delete_aggregateevent"), + ] + + operations = [ + migrations.CreateModel( + name="ParkAreaEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("description", models.TextField(blank=True)), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ParkEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("description", models.TextField(blank=True)), + ( + "status", + models.CharField( + choices=[ + ("OPERATING", "Operating"), + ("CLOSED_TEMP", "Temporarily Closed"), + ("CLOSED_PERM", "Permanently Closed"), + ("UNDER_CONSTRUCTION", "Under Construction"), + ("DEMOLISHED", "Demolished"), + ("RELOCATED", "Relocated"), + ], + default="OPERATING", + max_length=20, + ), + ), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ("operating_season", models.CharField(blank=True, max_length=255)), + ( + "size_acres", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ("website", models.URLField(blank=True)), + ( + "average_rating", + models.DecimalField( + blank=True, decimal_places=2, max_digits=3, null=True + ), + ), + ("ride_count", models.IntegerField(blank=True, null=True)), + ("coaster_count", models.IntegerField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.RemoveField( + model_name="historicalpark", + name="history_user", + ), + migrations.RemoveField( + model_name="historicalpark", + name="owner", + ), + migrations.RemoveField( + model_name="historicalparkarea", + name="history_user", + ), + migrations.RemoveField( + model_name="historicalparkarea", + name="park", + ), + pgtrigger.migrations.AddTrigger( + model_name="park", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parkevent" ("average_rating", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "owner_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."owner_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="83eb12a74769e2601a23691085a345c29c9b6f68", + 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", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "owner_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."owner_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;', + hash="f42a468ec35a2d51abd5c1ae1afa41b300ae0a1b", + operation="UPDATE", + pgid="pgtrigger_update_update_19f56", + table="parks_park", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parkarea", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;', + hash="fa64ee07f872bf2214b2c1b638b028429752bac4", + operation="INSERT", + pgid="pgtrigger_insert_insert_13457", + table="parks_parkarea", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="parkarea", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;', + hash="59fa84527a4fd0fa51685058b6037fa22163a095", + operation="UPDATE", + pgid="pgtrigger_update_update_6e5aa", + table="parks_parkarea", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="parkareaevent", + name="park", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.park", + ), + ), + migrations.AddField( + model_name="parkareaevent", + name="pgh_context", + field=models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="pghistory.context", + ), + ), + migrations.AddField( + model_name="parkareaevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.parkarea", + ), + ), + migrations.AddField( + model_name="parkevent", + name="owner", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="companies.company", + ), + ), + migrations.AddField( + model_name="parkevent", + name="pgh_context", + field=models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="pghistory.context", + ), + ), + migrations.AddField( + model_name="parkevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="parks.park", + ), + ), + migrations.DeleteModel( + name="HistoricalPark", + ), + migrations.DeleteModel( + name="HistoricalParkArea", + ), + ] diff --git a/parks/models.py b/parks/models.py index d6ed5903..1033cdbe 100644 --- a/parks/models.py +++ b/parks/models.py @@ -5,17 +5,18 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from decimal import Decimal, ROUND_DOWN, InvalidOperation from typing import Tuple, Optional, Any, TYPE_CHECKING +import pghistory from companies.models import Company from media.models import Photo -from history_tracking.models import HistoricalModel +from history_tracking.models import TrackedModel from location.models import Location if TYPE_CHECKING: from rides.models import Ride - -class Park(HistoricalModel): +@pghistory.track() +class Park(TrackedModel): id: int # Type hint for Django's automatic id field STATUS_CHOICES = [ ("OPERATING", "Operating"), @@ -101,17 +102,21 @@ class Park(HistoricalModel): try: return cls.objects.get(slug=slug), False except cls.DoesNotExist: - # Check historical slugs - history = cls.history.filter(slug=slug).order_by("-history_date").first() # type: ignore[attr-defined] + # Check historical slugs using pghistory + history_model = cls.get_history_model() + history = history_model.objects.filter( + slug=slug + ).order_by('-pgh_created_at').first() + if history: try: - return cls.objects.get(pk=history.instance.pk), True + return cls.objects.get(pk=history.pgh_obj_id), True except cls.DoesNotExist as e: raise cls.DoesNotExist("No park found with this slug") from e raise cls.DoesNotExist("No park found with this slug") - -class ParkArea(HistoricalModel): +@pghistory.track() +class ParkArea(TrackedModel): id: int # Type hint for Django's automatic id field park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas") name = models.CharField(max_length=255) @@ -148,11 +153,15 @@ class ParkArea(HistoricalModel): try: return cls.objects.get(slug=slug), False except cls.DoesNotExist: - # Check historical slugs - history = cls.history.filter(slug=slug).order_by("-history_date").first() # type: ignore[attr-defined] + # Check historical slugs using pghistory + history_model = cls.get_history_model() + history = history_model.objects.filter( + slug=slug + ).order_by('-pgh_created_at').first() + if history: try: - return cls.objects.get(pk=history.instance.pk), True + return cls.objects.get(pk=history.pgh_obj_id), True except cls.DoesNotExist as e: raise cls.DoesNotExist("No park area found with this slug") from e raise cls.DoesNotExist("No park area found with this slug") diff --git a/rides/forms.py b/rides/forms.py index cf82a8e8..eea0c388 100644 --- a/rides/forms.py +++ b/rides/forms.py @@ -3,7 +3,8 @@ from django.forms import ModelChoiceField from django.urls import reverse_lazy from .models import Ride, RideModel from parks.models import Park, ParkArea -from companies.models import Manufacturer, Designer +from companies.models import Manufacturer +from designers.models import Designer class RideForm(forms.ModelForm): diff --git a/rides/migrations/0010_rideevent_ridemodelevent_and_more.py b/rides/migrations/0010_rideevent_ridemodelevent_and_more.py new file mode 100644 index 00000000..18d193c1 --- /dev/null +++ b/rides/migrations/0010_rideevent_ridemodelevent_and_more.py @@ -0,0 +1,364 @@ +# Generated by Django 5.1.4 on 2025-02-09 15:31 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("companies", "0003_companyevent_manufacturerevent"), + ( + "designers", + "0002_designerevent_remove_historicaldesigner_history_user_and_more", + ), + ("parks", "0001_initial"), + ("pghistory", "0006_delete_aggregateevent"), + ("rides", "0009_remove_historicalride_model_name_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="RideEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(db_index=False, max_length=255)), + ("description", models.TextField(blank=True)), + ( + "category", + models.CharField( + blank=True, + choices=[ + ("", "Select ride type"), + ("RC", "Roller Coaster"), + ("DR", "Dark Ride"), + ("FR", "Flat Ride"), + ("WR", "Water Ride"), + ("TR", "Transport"), + ("OT", "Other"), + ], + default="", + max_length=2, + ), + ), + ( + "status", + models.CharField( + choices=[ + ("OPERATING", "Operating"), + ("SBNO", "Standing But Not Operating"), + ("CLOSING", "Closing"), + ("CLOSED_PERM", "Permanently Closed"), + ("UNDER_CONSTRUCTION", "Under Construction"), + ("DEMOLISHED", "Demolished"), + ("RELOCATED", "Relocated"), + ], + default="OPERATING", + max_length=20, + ), + ), + ( + "post_closing_status", + models.CharField( + blank=True, + choices=[ + ("SBNO", "Standing But Not Operating"), + ("CLOSED_PERM", "Permanently Closed"), + ], + help_text="Status to change to after closing date", + max_length=20, + null=True, + ), + ), + ("opening_date", models.DateField(blank=True, null=True)), + ("closing_date", models.DateField(blank=True, null=True)), + ("status_since", models.DateField(blank=True, null=True)), + ("min_height_in", models.PositiveIntegerField(blank=True, null=True)), + ("max_height_in", models.PositiveIntegerField(blank=True, null=True)), + ( + "capacity_per_hour", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "ride_duration_seconds", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "average_rating", + models.DecimalField( + blank=True, decimal_places=2, max_digits=3, null=True + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="RideModelEvent", + fields=[ + ("pgh_id", models.AutoField(primary_key=True, serialize=False)), + ("pgh_created_at", models.DateTimeField(auto_now_add=True)), + ("pgh_label", models.TextField(help_text="The event label.")), + ("id", models.BigIntegerField()), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True)), + ( + "category", + models.CharField( + blank=True, + choices=[ + ("", "Select ride type"), + ("RC", "Roller Coaster"), + ("DR", "Dark Ride"), + ("FR", "Flat Ride"), + ("WR", "Water Ride"), + ("TR", "Transport"), + ("OT", "Other"), + ], + default="", + max_length=2, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.RemoveField( + model_name="historicalride", + name="designer", + ), + migrations.RemoveField( + model_name="historicalride", + name="history_user", + ), + migrations.RemoveField( + model_name="historicalride", + name="manufacturer", + ), + migrations.RemoveField( + model_name="historicalride", + name="park", + ), + migrations.RemoveField( + model_name="historicalride", + name="park_area", + ), + migrations.RemoveField( + model_name="historicalride", + name="ride_model", + ), + migrations.RemoveField( + model_name="historicalridemodel", + name="history_user", + ), + migrations.RemoveField( + model_name="historicalridemodel", + name="manufacturer", + ), + migrations.AlterField( + model_name="ride", + name="designer", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="rides", + to="designers.designer", + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ride", + trigger=pgtrigger.compiler.Trigger( + name="insert_insert", + sql=pgtrigger.compiler.UpsertTriggerSql( + func='INSERT INTO "rides_rideevent" ("average_rating", "capacity_per_hour", "category", "closing_date", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "park_area_id", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "slug", "status", "status_since", "updated_at") VALUES (NEW."average_rating", NEW."capacity_per_hour", NEW."category", NEW."closing_date", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."park_area_id", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at"); RETURN NULL;', + hash="870aa867ae6892f187dc0382e4a6833b5d1267c5", + operation="INSERT", + pgid="pgtrigger_insert_insert_52074", + table="rides_ride", + when="AFTER", + ), + ), + ), + pgtrigger.migrations.AddTrigger( + model_name="ride", + trigger=pgtrigger.compiler.Trigger( + name="update_update", + sql=pgtrigger.compiler.UpsertTriggerSql( + condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", + func='INSERT INTO "rides_rideevent" ("average_rating", "capacity_per_hour", "category", "closing_date", "created_at", "description", "designer_id", "id", "manufacturer_id", "max_height_in", "min_height_in", "name", "opening_date", "park_area_id", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "post_closing_status", "ride_duration_seconds", "ride_model_id", "slug", "status", "status_since", "updated_at") VALUES (NEW."average_rating", NEW."capacity_per_hour", NEW."category", NEW."closing_date", NEW."created_at", NEW."description", NEW."designer_id", NEW."id", NEW."manufacturer_id", NEW."max_height_in", NEW."min_height_in", NEW."name", NEW."opening_date", NEW."park_area_id", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."post_closing_status", NEW."ride_duration_seconds", NEW."ride_model_id", NEW."slug", NEW."status", NEW."status_since", NEW."updated_at"); RETURN NULL;', + hash="8bafb42256ee98b4517ae4d39d0e774111794fea", + 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", "id", "manufacturer_id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."id", NEW."manufacturer_id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."updated_at"); RETURN NULL;', + hash="e9e3c3ec4cb2400b363035534c580c94a3bb1d53", + 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", "id", "manufacturer_id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."id", NEW."manufacturer_id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."updated_at"); RETURN NULL;', + hash="4c8073b866beac402ace852e23974fcb01d24267", + operation="UPDATE", + pgid="pgtrigger_update_update_0ca1a", + table="rides_ridemodel", + when="AFTER", + ), + ), + ), + migrations.AddField( + model_name="rideevent", + name="designer", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="designers.designer", + ), + ), + migrations.AddField( + model_name="rideevent", + name="manufacturer", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="companies.manufacturer", + ), + ), + migrations.AddField( + model_name="rideevent", + name="park", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.park", + ), + ), + migrations.AddField( + model_name="rideevent", + name="park_area", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="parks.parkarea", + ), + ), + migrations.AddField( + model_name="rideevent", + name="pgh_context", + field=models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="pghistory.context", + ), + ), + migrations.AddField( + model_name="rideevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ride", + ), + ), + migrations.AddField( + model_name="rideevent", + name="ride_model", + field=models.ForeignKey( + blank=True, + db_constraint=False, + help_text="The specific model/type of this ride", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="rides.ridemodel", + ), + ), + migrations.AddField( + model_name="ridemodelevent", + name="manufacturer", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + related_query_name="+", + to="companies.manufacturer", + ), + ), + migrations.AddField( + model_name="ridemodelevent", + name="pgh_context", + field=models.ForeignKey( + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="pghistory.context", + ), + ), + migrations.AddField( + model_name="ridemodelevent", + name="pgh_obj", + field=models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="events", + to="rides.ridemodel", + ), + ), + migrations.DeleteModel( + name="HistoricalRide", + ), + migrations.DeleteModel( + name="HistoricalRideModel", + ), + ] diff --git a/rides/models.py b/rides/models.py index 1374a759..1d1815f8 100644 --- a/rides/models.py +++ b/rides/models.py @@ -1,8 +1,8 @@ from django.db import models from django.utils.text import slugify from django.contrib.contenttypes.fields import GenericRelation -from history_tracking.models import HistoricalModel - +from history_tracking.models import TrackedModel +import pghistory # Shared choices that will be used by multiple models CATEGORY_CHOICES = [ @@ -15,8 +15,8 @@ CATEGORY_CHOICES = [ ('OT', 'Other'), ] - -class RideModel(HistoricalModel): +@pghistory.track() +class RideModel(TrackedModel): """ Represents a specific model/type of ride that can be manufactured by different companies. For example: B&M Dive Coaster, Vekoma Boomerang, etc. @@ -46,8 +46,8 @@ class RideModel(HistoricalModel): def __str__(self) -> str: return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}" - -class Ride(HistoricalModel): +@pghistory.track() +class Ride(TrackedModel): STATUS_CHOICES = [ ('OPERATING', 'Operating'), ('SBNO', 'Standing But Not Operating'), @@ -91,7 +91,7 @@ class Ride(HistoricalModel): blank=True ) designer = models.ForeignKey( - 'companies.Designer', + 'designers.Designer', # Updated to point to the new Designer model on_delete=models.SET_NULL, related_name='rides', null=True, @@ -147,7 +147,6 @@ class Ride(HistoricalModel): self.slug = slugify(self.name) super().save(*args, **kwargs) - class RollerCoasterStats(models.Model): TRACK_MATERIAL_CHOICES = [ ('STEEL', 'Steel'), diff --git a/rides/views.py b/rides/views.py index 14a08ab4..610d8788 100644 --- a/rides/views.py +++ b/rides/views.py @@ -30,7 +30,8 @@ from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, History from moderation.models import EditSubmission from media.models import Photo from accounts.models import User -from companies.models import Manufacturer, Designer +from companies.models import Manufacturer +from designers.models import Designer def show_coaster_fields(request: HttpRequest) -> HttpResponse: