Refactor model imports and update admin classes to use pghistory for historical tracking; replace HistoricalModel with TrackedModel in relevant models

This commit is contained in:
pacnpal
2025-02-09 11:20:40 -05:00
parent 52cb51cb14
commit 64d9943d86
24 changed files with 1729 additions and 137 deletions

View File

@@ -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',)}

View File

@@ -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,
},
),
]

View File

@@ -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",
),
),
]

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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",
),
]

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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",
),
]

View File

@@ -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 = [

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
),
),
]

View File

@@ -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"),

View File

@@ -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)

View File

@@ -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",
),
]

View File

@@ -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")

View File

@@ -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):

View File

@@ -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",
),
]

View File

@@ -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'),

View File

@@ -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: