From ccbffc5191beb8dc4ca0622ee9017ee8e60d3c24 Mon Sep 17 00:00:00 2001 From: vjpixel Date: Mon, 4 May 2026 23:51:10 -0300 Subject: [PATCH] fix(#891): Replace TimeStampedModel inheritance with explicit fields and indexes Remove TimeStampedModel inheritance from PostImage, Clipping, Post, Sound, Marker, Object, Artwork, and Exhibit. Declare created/modified manually with descending Meta.indexes on each model, plus a composite (exhibit_type, -created) index on Exhibit covering the filter+order pattern in users/views.py:109,112. Keeps django-extensions installed so historical migrations (blog/0008, core/0013, 0016, 0027) still replay on fresh local environments. Addresses @pablodiegoss's review on PR #921: indexing on top of the inheritance was a band-aid that locked the project into TimeStampedModel forever. Co-Authored-By: Claude Sonnet 4.6 --- ...ed_alter_clipping_display_date_and_more.py | 73 ++++++++ src/blog/models.py | 34 +++- ...created_alter_artwork_modified_and_more.py | 158 ++++++++++++++++++ src/core/models.py | 60 ++++++- 4 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 src/blog/migrations/0010_alter_clipping_created_alter_clipping_display_date_and_more.py create mode 100644 src/core/migrations/0028_alter_artwork_created_alter_artwork_modified_and_more.py diff --git a/src/blog/migrations/0010_alter_clipping_created_alter_clipping_display_date_and_more.py b/src/blog/migrations/0010_alter_clipping_created_alter_clipping_display_date_and_more.py new file mode 100644 index 00000000..8a6230c7 --- /dev/null +++ b/src/blog/migrations/0010_alter_clipping_created_alter_clipping_display_date_and_more.py @@ -0,0 +1,73 @@ +# Generated by Django 6.0.3 on 2026-05-05 02:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0009_update_clipping_display_dates'), + ('users', '0012_profileevent_profile_insert_insert_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='clipping', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='clipping', + name='display_date', + field=models.DateField(db_index=True), + ), + migrations.AlterField( + model_name='clipping', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='post', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='post', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='postimage', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='postimage', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddIndex( + model_name='clipping', + index=models.Index(fields=['-created'], name='clipping_created_desc_idx'), + ), + migrations.AddIndex( + model_name='clipping', + index=models.Index(fields=['-modified'], name='clipping_modified_desc_idx'), + ), + migrations.AddIndex( + model_name='post', + index=models.Index(fields=['-created'], name='post_created_desc_idx'), + ), + migrations.AddIndex( + model_name='post', + index=models.Index(fields=['-modified'], name='post_modified_desc_idx'), + ), + migrations.AddIndex( + model_name='postimage', + index=models.Index(fields=['-created'], name='postimage_created_desc_idx'), + ), + migrations.AddIndex( + model_name='postimage', + index=models.Index(fields=['-modified'], name='postimage_modified_desc_idx'), + ), + ] diff --git a/src/blog/models.py b/src/blog/models.py index fc93cf8e..77224dbf 100644 --- a/src/blog/models.py +++ b/src/blog/models.py @@ -1,6 +1,5 @@ from django.core.files.storage import default_storage from django.db import models -from django_extensions.db.models import TimeStampedModel from users.models import Profile @@ -22,27 +21,45 @@ class Meta: verbose_name_plural = "categories" -class PostImage(TimeStampedModel): +class PostImage(models.Model): file = models.FileField(storage=default_storage, upload_to=IMAGE_BASE_PATH) description = models.CharField(max_length=500, blank=True) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="postimage_created_desc_idx"), + models.Index(fields=["-modified"], name="postimage_modified_desc_idx"), + ] def __str__(self): return self.file.name.lstrip(IMAGE_BASE_PATH) -class Clipping(TimeStampedModel): +class Clipping(models.Model): id = models.BigAutoField(primary_key=True) title = models.CharField(max_length=200) description = models.CharField(max_length=500) link = models.URLField() file = models.FileField(upload_to="clipping_files/") display_date = models.DateField(db_index=True) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="clipping_created_desc_idx"), + models.Index(fields=["-modified"], name="clipping_modified_desc_idx"), + ] def __str__(self): return self.title -class Post(TimeStampedModel): +class Post(models.Model): id = models.BigAutoField(primary_key=True) title = models.CharField(max_length=200) status = models.CharField( @@ -58,6 +75,15 @@ class Post(TimeStampedModel): body = models.TextField() categories = models.ManyToManyField(Category, related_name="posts", blank=True) images = models.ManyToManyField(PostImage, related_name="posts", blank=True) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="post_created_desc_idx"), + models.Index(fields=["-modified"], name="post_modified_desc_idx"), + ] def __str__(self): return self.title diff --git a/src/core/migrations/0028_alter_artwork_created_alter_artwork_modified_and_more.py b/src/core/migrations/0028_alter_artwork_created_alter_artwork_modified_and_more.py new file mode 100644 index 00000000..8a293b89 --- /dev/null +++ b/src/core/migrations/0028_alter_artwork_created_alter_artwork_modified_and_more.py @@ -0,0 +1,158 @@ +# Generated by Django 6.0.3 on 2026-05-05 02:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0027_sound_soundevent_remove_artwork_insert_insert_and_more'), + ('users', '0012_profileevent_profile_insert_insert_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='artwork', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='artwork', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='artworkevent', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='artworkevent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='exhibit', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='exhibit', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='exhibitevent', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='exhibitevent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='marker', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='marker', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='markerevent', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='markerevent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='object', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='object', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='objectevent', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='objectevent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='sound', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='sound', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AlterField( + model_name='soundevent', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='created'), + ), + migrations.AlterField( + model_name='soundevent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='modified'), + ), + migrations.AddIndex( + model_name='artwork', + index=models.Index(fields=['-created'], name='artwork_created_desc_idx'), + ), + migrations.AddIndex( + model_name='artwork', + index=models.Index(fields=['-modified'], name='artwork_modified_desc_idx'), + ), + migrations.AddIndex( + model_name='exhibit', + index=models.Index(fields=['-created'], name='exhibit_created_desc_idx'), + ), + migrations.AddIndex( + model_name='exhibit', + index=models.Index(fields=['-modified'], name='exhibit_modified_desc_idx'), + ), + migrations.AddIndex( + model_name='exhibit', + index=models.Index(fields=['exhibit_type', '-created'], name='exhibit_type_created_idx'), + ), + migrations.AddIndex( + model_name='marker', + index=models.Index(fields=['-created'], name='marker_created_desc_idx'), + ), + migrations.AddIndex( + model_name='marker', + index=models.Index(fields=['-modified'], name='marker_modified_desc_idx'), + ), + migrations.AddIndex( + model_name='object', + index=models.Index(fields=['-created'], name='object_created_desc_idx'), + ), + migrations.AddIndex( + model_name='object', + index=models.Index(fields=['-modified'], name='object_modified_desc_idx'), + ), + migrations.AddIndex( + model_name='sound', + index=models.Index(fields=['-created'], name='sound_created_desc_idx'), + ), + migrations.AddIndex( + model_name='sound', + index=models.Index(fields=['-modified'], name='sound_modified_desc_idx'), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index 7406e2ae..eec11dfc 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -7,7 +7,6 @@ from django.dispatch import receiver from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from django_extensions.db.models import TimeStampedModel from fast_html import a, audio, b, div, h1, img, p, render, span, video from PIL import Image from pymarker.core import generate_marker_from_image, generate_patt_from_image @@ -100,7 +99,7 @@ class SoundExtensions(models.TextChoices): @pghistory.track() -class Sound(TimeStampedModel, ContentMixin): +class Sound(models.Model, ContentMixin): file = models.FileField(upload_to="sounds/") title = models.CharField(max_length=50, blank=False) author = models.CharField(max_length=60, blank=False) @@ -113,6 +112,15 @@ class Sound(TimeStampedModel, ContentMixin): file_extension = models.CharField( max_length=10, db_index=True, choices=SoundExtensions.choices ) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="sound_created_desc_idx"), + models.Index(fields=["-modified"], name="sound_modified_desc_idx"), + ] @property def date(self): @@ -208,7 +216,7 @@ class ExhibitTypes(models.TextChoices): @pghistory.track() -class Marker(TimeStampedModel, ContentMixin): +class Marker(models.Model, ContentMixin): owner = models.ForeignKey( Profile, on_delete=models.DO_NOTHING, related_name="markers" ) @@ -219,6 +227,15 @@ class Marker(TimeStampedModel, ContentMixin): # Save the file size of the Marker, so we avoid making requests to S3 / MinIO to check for it. file_size = models.IntegerField(default=0, blank=True, null=True) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="marker_created_desc_idx"), + models.Index(fields=["-modified"], name="marker_modified_desc_idx"), + ] def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -293,7 +310,7 @@ class ObjectExtensions(models.TextChoices): @pghistory.track() -class Object(TimeStampedModel, ContentMixin): +class Object(models.Model, ContentMixin): owner = models.ForeignKey( Profile, on_delete=models.DO_NOTHING, related_name="ar_objects" ) @@ -321,6 +338,15 @@ class Object(TimeStampedModel, ContentMixin): blank=True, null=True, ) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="object_created_desc_idx"), + models.Index(fields=["-modified"], name="object_modified_desc_idx"), + ] def __str__(self): return self.source.name @@ -410,7 +436,7 @@ def as_html_thumbnail(self, editable=False): @pghistory.track() -class Artwork(TimeStampedModel, ContentMixin): +class Artwork(models.Model, ContentMixin): author = models.ForeignKey( Profile, on_delete=models.DO_NOTHING, related_name="artworks" ) @@ -433,6 +459,15 @@ class Artwork(TimeStampedModel, ContentMixin): scale_y = models.FloatField(default=1.0) position_x = models.FloatField(default=0.0) position_y = models.FloatField(default=0.0) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="artwork_created_desc_idx"), + models.Index(fields=["-modified"], name="artwork_modified_desc_idx"), + ] @property def exhibits_count(self): @@ -493,7 +528,7 @@ def as_html_thumbnail(self, editable=False): @pghistory.track() -class Exhibit(TimeStampedModel, ContentMixin, models.Model): +class Exhibit(models.Model, ContentMixin): owner = models.ForeignKey( Profile, on_delete=models.DO_NOTHING, related_name="exhibits" ) @@ -511,6 +546,19 @@ class Exhibit(TimeStampedModel, ContentMixin, models.Model): default=ExhibitTypes.AR, db_index=True, ) + created = models.DateTimeField(auto_now_add=True, verbose_name="created") + modified = models.DateTimeField(auto_now=True, verbose_name="modified") + + class Meta: + get_latest_by = "modified" + indexes = [ + models.Index(fields=["-created"], name="exhibit_created_desc_idx"), + models.Index(fields=["-modified"], name="exhibit_modified_desc_idx"), + models.Index( + fields=["exhibit_type", "-created"], + name="exhibit_type_created_idx", + ), + ] def __str__(self): return self.name