mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:51:09 -05:00
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:
@@ -1,16 +1,15 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from simple_history.admin import SimpleHistoryAdmin
|
|
||||||
from .models import Company, Manufacturer
|
from .models import Company, Manufacturer
|
||||||
|
|
||||||
@admin.register(Company)
|
@admin.register(Company)
|
||||||
class CompanyAdmin(SimpleHistoryAdmin):
|
class CompanyAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'headquarters', 'website', 'created_at')
|
list_display = ('id', 'name', 'headquarters', 'website', 'created_at')
|
||||||
search_fields = ('name', 'headquarters', 'description')
|
search_fields = ('name', 'headquarters', 'description')
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
readonly_fields = ('created_at', 'updated_at')
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
|
||||||
@admin.register(Manufacturer)
|
@admin.register(Manufacturer)
|
||||||
class ManufacturerAdmin(SimpleHistoryAdmin):
|
class ManufacturerAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'headquarters', 'website', 'created_at')
|
list_display = ('id', 'name', 'headquarters', 'website', 'created_at')
|
||||||
search_fields = ('name', 'headquarters', 'description')
|
search_fields = ('name', 'headquarters', 'description')
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
|||||||
55
companies/migrations/0003_companyevent_manufacturerevent.py
Normal file
55
companies/migrations/0003_companyevent_manufacturerevent.py
Normal 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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,11 +2,11 @@ from django.db import models
|
|||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from typing import Tuple, Optional, ClassVar, TYPE_CHECKING
|
from typing import Tuple, Optional, ClassVar, TYPE_CHECKING
|
||||||
|
import pghistory
|
||||||
|
from history_tracking.models import TrackedModel, HistoricalSlug
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
@pghistory.track()
|
||||||
from history_tracking.models import HistoricalSlug
|
class Company(TrackedModel):
|
||||||
|
|
||||||
class Company(models.Model):
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
slug = models.SlugField(max_length=255, unique=True)
|
slug = models.SlugField(max_length=255, unique=True)
|
||||||
website = models.URLField(blank=True)
|
website = models.URLField(blank=True)
|
||||||
@@ -37,8 +37,18 @@ class Company(models.Model):
|
|||||||
try:
|
try:
|
||||||
return cls.objects.get(slug=slug), False
|
return cls.objects.get(slug=slug), False
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
# Check historical slugs
|
# Check pghistory first
|
||||||
from history_tracking.models import HistoricalSlug
|
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:
|
try:
|
||||||
historical = HistoricalSlug.objects.get(
|
historical = HistoricalSlug.objects.get(
|
||||||
content_type__model='company',
|
content_type__model='company',
|
||||||
@@ -48,7 +58,8 @@ class Company(models.Model):
|
|||||||
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
|
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
|
||||||
raise cls.DoesNotExist()
|
raise cls.DoesNotExist()
|
||||||
|
|
||||||
class Manufacturer(models.Model):
|
@pghistory.track()
|
||||||
|
class Manufacturer(TrackedModel):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
slug = models.SlugField(max_length=255, unique=True)
|
slug = models.SlugField(max_length=255, unique=True)
|
||||||
website = models.URLField(blank=True)
|
website = models.URLField(blank=True)
|
||||||
@@ -78,8 +89,18 @@ class Manufacturer(models.Model):
|
|||||||
try:
|
try:
|
||||||
return cls.objects.get(slug=slug), False
|
return cls.objects.get(slug=slug), False
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
# Check historical slugs
|
# Check pghistory first
|
||||||
from history_tracking.models import HistoricalSlug
|
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:
|
try:
|
||||||
historical = HistoricalSlug.objects.get(
|
historical = HistoricalSlug.objects.get(
|
||||||
content_type__model='manufacturer',
|
content_type__model='manufacturer',
|
||||||
@@ -88,43 +109,3 @@ class Manufacturer(models.Model):
|
|||||||
return cls.objects.get(pk=historical.object_id), True
|
return cls.objects.get(pk=historical.object_id), True
|
||||||
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
|
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
|
||||||
raise 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()
|
|
||||||
|
|||||||
@@ -2,18 +2,7 @@ from django.db import models
|
|||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
import pghistory
|
from history_tracking.models import TrackedModel
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
class SlugHistory(models.Model):
|
class SlugHistory(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -38,11 +27,9 @@ class SlugHistory(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Old slug '{self.old_slug}' for {self.content_object}"
|
return f"Old slug '{self.old_slug}' for {self.content_object}"
|
||||||
|
|
||||||
@pghistory.track()
|
class SluggedModel(TrackedModel):
|
||||||
class SluggedModel(HistoricalModel):
|
|
||||||
"""
|
"""
|
||||||
Abstract base model that provides slug functionality with history tracking.
|
Abstract base model that provides slug functionality with history tracking.
|
||||||
Inherits from HistoricalModel to get universal history tracking.
|
|
||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
slug = models.SlugField(max_length=200, unique=True)
|
slug = models.SlugField(max_length=200, unique=True)
|
||||||
@@ -69,7 +56,6 @@ class SluggedModel(HistoricalModel):
|
|||||||
if not self.slug:
|
if not self.slug:
|
||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
|
|
||||||
# Call HistoricalModel's save to ensure history tracking
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_id_field_name(self):
|
def get_id_field_name(self):
|
||||||
@@ -91,7 +77,18 @@ class SluggedModel(HistoricalModel):
|
|||||||
# Try to get by current slug first
|
# Try to get by current slug first
|
||||||
return cls.objects.get(slug=slug), False
|
return cls.objects.get(slug=slug), False
|
||||||
except cls.DoesNotExist:
|
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(
|
history = SlugHistory.objects.filter(
|
||||||
content_type=ContentType.objects.get_for_model(cls),
|
content_type=ContentType.objects.get_for_model(cls),
|
||||||
old_slug=slug
|
old_slug=slug
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from simple_history.admin import SimpleHistoryAdmin
|
from django.utils.text import slugify
|
||||||
from .models import Designer
|
from .models import Designer
|
||||||
|
|
||||||
@admin.register(Designer)
|
@admin.register(Designer)
|
||||||
class DesignerAdmin(SimpleHistoryAdmin):
|
class DesignerAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'headquarters', 'founded_date', 'website')
|
list_display = ('name', 'headquarters', 'founded_date', 'website')
|
||||||
search_fields = ('name', 'headquarters')
|
search_fields = ('name', 'headquarters')
|
||||||
list_filter = ('founded_date',)
|
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super().get_queryset(request).select_related()
|
||||||
|
|||||||
@@ -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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import slugify
|
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)
|
name = models.CharField(max_length=255)
|
||||||
slug = models.SlugField(max_length=255, unique=True)
|
slug = models.SlugField(max_length=255, unique=True)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
@@ -11,7 +13,6 @@ class Designer(models.Model):
|
|||||||
headquarters = models.CharField(max_length=255, blank=True)
|
headquarters = models.CharField(max_length=255, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
history = HistoricalRecords()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@@ -30,8 +31,13 @@ class Designer(models.Model):
|
|||||||
try:
|
try:
|
||||||
return cls.objects.get(slug=slug), False
|
return cls.objects.get(slug=slug), False
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
# Check historical slugs
|
# Check historical slugs using pghistory
|
||||||
history = cls.history.filter(slug=slug).order_by('-history_date').first()
|
history_model = cls.get_history_model()
|
||||||
|
history = (
|
||||||
|
history_model.objects.filter(slug=slug)
|
||||||
|
.order_by('-pgh_created_at')
|
||||||
|
.first()
|
||||||
|
)
|
||||||
if history:
|
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")
|
raise cls.DoesNotExist("No designer found with this slug")
|
||||||
|
|||||||
@@ -7,20 +7,9 @@ class HistoryTrackingConfig(AppConfig):
|
|||||||
name = "history_tracking"
|
name = "history_tracking"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from django.apps import apps
|
"""
|
||||||
from .mixins import HistoricalChangeMixin
|
No initialization needed for pghistory tracking.
|
||||||
|
History tracking is handled by the @pghistory.track() decorator
|
||||||
# Get the Park model
|
and triggers installed in migrations.
|
||||||
try:
|
"""
|
||||||
Park = apps.get_model('parks', 'Park')
|
pass
|
||||||
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
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from simple_history.models import HistoricalRecords
|
from simple_history.models import HistoricalRecords
|
||||||
from .mixins import HistoricalChangeMixin
|
from .mixins import HistoricalChangeMixin
|
||||||
from typing import Any, Type, TypeVar, cast
|
from typing import Any, Type, TypeVar, cast
|
||||||
@@ -9,8 +10,46 @@ from django.db.models import QuerySet
|
|||||||
|
|
||||||
T = TypeVar('T', bound=models.Model)
|
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):
|
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)
|
id = models.BigAutoField(primary_key=True)
|
||||||
history: HistoricalRecords = HistoricalRecords(
|
history: HistoricalRecords = HistoricalRecords(
|
||||||
inherit=True,
|
inherit=True,
|
||||||
@@ -30,6 +69,27 @@ class HistoricalModel(models.Model):
|
|||||||
model = self._history_model
|
model = self._history_model
|
||||||
return model.objects.filter(id=self.pk).order_by('-history_date')
|
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):
|
class HistoricalSlug(models.Model):
|
||||||
"""Track historical slugs for models"""
|
"""Track historical slugs for models"""
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
|||||||
@@ -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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -3,10 +3,12 @@ from django.db import models
|
|||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from simple_history.models import HistoricalRecords
|
|
||||||
from django.contrib.gis.geos import Point
|
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
|
A generic location model that can be associated with any model
|
||||||
using GenericForeignKey. Stores detailed location information
|
using GenericForeignKey. Stores detailed location information
|
||||||
@@ -63,7 +65,6 @@ class Location(models.Model):
|
|||||||
# Metadata
|
# Metadata
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
history = HistoricalRecords()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
|
|||||||
90
memory-bank/decisions/history-tracking-migration.md
Normal file
90
memory-bank/decisions/history-tracking-migration.md
Normal 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
|
||||||
98
memory-bank/decisions/migration-progress.md
Normal file
98
memory-bank/decisions/migration-progress.md
Normal 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
|
||||||
@@ -3,7 +3,6 @@ from django.contrib.admin import AdminSite
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
import pghistory
|
|
||||||
from .models import EditSubmission, PhotoSubmission
|
from .models import EditSubmission, PhotoSubmission
|
||||||
|
|
||||||
class ModerationAdminSite(AdminSite):
|
class ModerationAdminSite(AdminSite):
|
||||||
@@ -77,7 +76,7 @@ class PhotoSubmissionAdmin(admin.ModelAdmin):
|
|||||||
obj.reject(request.user, obj.notes)
|
obj.reject(request.user, obj.notes)
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
class HistoryAdmin(admin.ModelAdmin):
|
class HistoryEventAdmin(admin.ModelAdmin):
|
||||||
"""Admin interface for viewing model history events"""
|
"""Admin interface for viewing model history events"""
|
||||||
list_display = ['pgh_label', 'pgh_created_at', 'get_object_link', 'get_context']
|
list_display = ['pgh_label', 'pgh_created_at', 'get_object_link', 'get_context']
|
||||||
list_filter = ['pgh_label', 'pgh_created_at']
|
list_filter = ['pgh_label', 'pgh_created_at']
|
||||||
@@ -106,4 +105,6 @@ class HistoryAdmin(admin.ModelAdmin):
|
|||||||
# Register with moderation site only
|
# Register with moderation site only
|
||||||
moderation_site.register(EditSubmission, EditSubmissionAdmin)
|
moderation_site.register(EditSubmission, EditSubmissionAdmin)
|
||||||
moderation_site.register(PhotoSubmission, PhotoSubmissionAdmin)
|
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)
|
||||||
|
|||||||
@@ -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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -10,12 +10,12 @@ from django.contrib.auth.base_user import AbstractBaseUser
|
|||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
import pghistory
|
import pghistory
|
||||||
from core.models import HistoricalModel
|
from history_tracking.models import TrackedModel
|
||||||
|
|
||||||
UserType = Union[AbstractBaseUser, AnonymousUser]
|
UserType = Union[AbstractBaseUser, AnonymousUser]
|
||||||
|
|
||||||
@pghistory.track() # Track all changes by default
|
@pghistory.track() # Track all changes by default
|
||||||
class EditSubmission(HistoricalModel):
|
class EditSubmission(TrackedModel):
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("PENDING", "Pending"),
|
("PENDING", "Pending"),
|
||||||
("APPROVED", "Approved"),
|
("APPROVED", "Approved"),
|
||||||
@@ -199,7 +199,7 @@ class EditSubmission(HistoricalModel):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@pghistory.track() # Track all changes by default
|
@pghistory.track() # Track all changes by default
|
||||||
class PhotoSubmission(HistoricalModel):
|
class PhotoSubmission(TrackedModel):
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("PENDING", "Pending"),
|
("PENDING", "Pending"),
|
||||||
("APPROVED", "Approved"),
|
("APPROVED", "Approved"),
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from simple_history.admin import SimpleHistoryAdmin
|
|
||||||
from .models import Park, ParkArea
|
from .models import Park, ParkArea
|
||||||
|
|
||||||
class ParkAdmin(SimpleHistoryAdmin):
|
class ParkAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'formatted_location', 'status', 'owner', 'created_at', 'updated_at')
|
list_display = ('name', 'formatted_location', 'status', 'owner', 'created_at', 'updated_at')
|
||||||
list_filter = ('status',)
|
list_filter = ('status',)
|
||||||
search_fields = ('name', 'description', 'location__name', 'location__city', 'location__country')
|
search_fields = ('name', 'description', 'location__name', 'location__city', 'location__country')
|
||||||
@@ -15,21 +14,13 @@ class ParkAdmin(SimpleHistoryAdmin):
|
|||||||
return obj.formatted_location
|
return obj.formatted_location
|
||||||
formatted_location.short_description = 'Location'
|
formatted_location.short_description = 'Location'
|
||||||
|
|
||||||
def get_history_list_display(self, request):
|
class ParkAreaAdmin(admin.ModelAdmin):
|
||||||
"""Customize the list display for history records"""
|
|
||||||
return ('name', 'formatted_location', 'status', 'history_date', 'history_user')
|
|
||||||
|
|
||||||
class ParkAreaAdmin(SimpleHistoryAdmin):
|
|
||||||
list_display = ('name', 'park', 'created_at', 'updated_at')
|
list_display = ('name', 'park', 'created_at', 'updated_at')
|
||||||
list_filter = ('park',)
|
list_filter = ('park',)
|
||||||
search_fields = ('name', 'description', 'park__name')
|
search_fields = ('name', 'description', 'park__name')
|
||||||
readonly_fields = ('created_at', 'updated_at')
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
prepopulated_fields = {'slug': ('name',)}
|
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
|
# Register the models with their admin classes
|
||||||
admin.site.register(Park, ParkAdmin)
|
admin.site.register(Park, ParkAdmin)
|
||||||
admin.site.register(ParkArea, ParkAreaAdmin)
|
admin.site.register(ParkArea, ParkAreaAdmin)
|
||||||
|
|||||||
233
parks/migrations/0002_parkareaevent_parkevent_and_more.py
Normal file
233
parks/migrations/0002_parkareaevent_parkevent_and_more.py
Normal 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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -5,17 +5,18 @@ from django.contrib.contenttypes.fields import GenericRelation
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from decimal import Decimal, ROUND_DOWN, InvalidOperation
|
from decimal import Decimal, ROUND_DOWN, InvalidOperation
|
||||||
from typing import Tuple, Optional, Any, TYPE_CHECKING
|
from typing import Tuple, Optional, Any, TYPE_CHECKING
|
||||||
|
import pghistory
|
||||||
|
|
||||||
from companies.models import Company
|
from companies.models import Company
|
||||||
from media.models import Photo
|
from media.models import Photo
|
||||||
from history_tracking.models import HistoricalModel
|
from history_tracking.models import TrackedModel
|
||||||
from location.models import Location
|
from location.models import Location
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from rides.models import Ride
|
from rides.models import Ride
|
||||||
|
|
||||||
|
@pghistory.track()
|
||||||
class Park(HistoricalModel):
|
class Park(TrackedModel):
|
||||||
id: int # Type hint for Django's automatic id field
|
id: int # Type hint for Django's automatic id field
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("OPERATING", "Operating"),
|
("OPERATING", "Operating"),
|
||||||
@@ -101,17 +102,21 @@ class Park(HistoricalModel):
|
|||||||
try:
|
try:
|
||||||
return cls.objects.get(slug=slug), False
|
return cls.objects.get(slug=slug), False
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
# Check historical slugs
|
# Check historical slugs using pghistory
|
||||||
history = cls.history.filter(slug=slug).order_by("-history_date").first() # type: ignore[attr-defined]
|
history_model = cls.get_history_model()
|
||||||
|
history = history_model.objects.filter(
|
||||||
|
slug=slug
|
||||||
|
).order_by('-pgh_created_at').first()
|
||||||
|
|
||||||
if history:
|
if history:
|
||||||
try:
|
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:
|
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") from e
|
||||||
raise cls.DoesNotExist("No park found with this slug")
|
raise cls.DoesNotExist("No park found with this slug")
|
||||||
|
|
||||||
|
@pghistory.track()
|
||||||
class ParkArea(HistoricalModel):
|
class ParkArea(TrackedModel):
|
||||||
id: int # Type hint for Django's automatic id field
|
id: int # Type hint for Django's automatic id field
|
||||||
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas")
|
park = models.ForeignKey(Park, on_delete=models.CASCADE, related_name="areas")
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
@@ -148,11 +153,15 @@ class ParkArea(HistoricalModel):
|
|||||||
try:
|
try:
|
||||||
return cls.objects.get(slug=slug), False
|
return cls.objects.get(slug=slug), False
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
# Check historical slugs
|
# Check historical slugs using pghistory
|
||||||
history = cls.history.filter(slug=slug).order_by("-history_date").first() # type: ignore[attr-defined]
|
history_model = cls.get_history_model()
|
||||||
|
history = history_model.objects.filter(
|
||||||
|
slug=slug
|
||||||
|
).order_by('-pgh_created_at').first()
|
||||||
|
|
||||||
if history:
|
if history:
|
||||||
try:
|
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:
|
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") from e
|
||||||
raise cls.DoesNotExist("No park area found with this slug")
|
raise cls.DoesNotExist("No park area found with this slug")
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from django.forms import ModelChoiceField
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from .models import Ride, RideModel
|
from .models import Ride, RideModel
|
||||||
from parks.models import Park, ParkArea
|
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):
|
class RideForm(forms.ModelForm):
|
||||||
|
|||||||
364
rides/migrations/0010_rideevent_ridemodelevent_and_more.py
Normal file
364
rides/migrations/0010_rideevent_ridemodelevent_and_more.py
Normal 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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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="[AWS-SECRET-REMOVED]",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
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
|
# Shared choices that will be used by multiple models
|
||||||
CATEGORY_CHOICES = [
|
CATEGORY_CHOICES = [
|
||||||
@@ -15,8 +15,8 @@ CATEGORY_CHOICES = [
|
|||||||
('OT', 'Other'),
|
('OT', 'Other'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@pghistory.track()
|
||||||
class RideModel(HistoricalModel):
|
class RideModel(TrackedModel):
|
||||||
"""
|
"""
|
||||||
Represents a specific model/type of ride that can be manufactured by different companies.
|
Represents a specific model/type of ride that can be manufactured by different companies.
|
||||||
For example: B&M Dive Coaster, Vekoma Boomerang, etc.
|
For example: B&M Dive Coaster, Vekoma Boomerang, etc.
|
||||||
@@ -46,8 +46,8 @@ class RideModel(HistoricalModel):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}"
|
return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}"
|
||||||
|
|
||||||
|
@pghistory.track()
|
||||||
class Ride(HistoricalModel):
|
class Ride(TrackedModel):
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('OPERATING', 'Operating'),
|
('OPERATING', 'Operating'),
|
||||||
('SBNO', 'Standing But Not Operating'),
|
('SBNO', 'Standing But Not Operating'),
|
||||||
@@ -91,7 +91,7 @@ class Ride(HistoricalModel):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
designer = models.ForeignKey(
|
designer = models.ForeignKey(
|
||||||
'companies.Designer',
|
'designers.Designer', # Updated to point to the new Designer model
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name='rides',
|
related_name='rides',
|
||||||
null=True,
|
null=True,
|
||||||
@@ -147,7 +147,6 @@ class Ride(HistoricalModel):
|
|||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RollerCoasterStats(models.Model):
|
class RollerCoasterStats(models.Model):
|
||||||
TRACK_MATERIAL_CHOICES = [
|
TRACK_MATERIAL_CHOICES = [
|
||||||
('STEEL', 'Steel'),
|
('STEEL', 'Steel'),
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, History
|
|||||||
from moderation.models import EditSubmission
|
from moderation.models import EditSubmission
|
||||||
from media.models import Photo
|
from media.models import Photo
|
||||||
from accounts.models import User
|
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:
|
def show_coaster_fields(request: HttpRequest) -> HttpResponse:
|
||||||
|
|||||||
Reference in New Issue
Block a user