Refactor comments app to use mixins for comment functionality; update admin interfaces and add historical model fixes

This commit is contained in:
pacnpal
2025-02-08 16:33:55 -05:00
parent 75f5b07129
commit 03f9df4bab
21 changed files with 548 additions and 280 deletions

View File

@@ -0,0 +1,61 @@
from django.db import models
from simple_history.models import HistoricalRecords
from django.contrib.contenttypes.fields import GenericRelation
from django.utils.timezone import now
class CustomHistoricalRecords(HistoricalRecords):
"""Custom historical records that properly handle generic relations."""
def copy_fields(self, model):
"""
Copy fields from the model to the historical record model,
excluding GenericRelation fields.
"""
fields = {}
for field in model._meta.concrete_fields:
if not isinstance(field, GenericRelation) and field.name not in [
'comments', 'comment_threads', 'photos', 'reviews'
]:
fields[field.name] = field.clone()
return fields
def create_history_model(self, model, inherited):
"""
Override to ensure we don't create duplicate auto fields.
"""
attrs = {
'__module__': model.__module__,
'_history_excluded_fields': ['comments', 'comment_threads', 'photos', 'reviews'],
}
app_module = '%s.models' % model._meta.app_label
if inherited:
# inherited use models.AutoField instead of models.IntegerField
attrs.update({
'id': models.AutoField(primary_key=True),
'history_id': models.AutoField(primary_key=True),
'history_date': models.DateTimeField(default=now),
'history_change_reason': models.CharField(max_length=100, null=True),
'history_type': models.CharField(max_length=1, choices=(
('+', 'Created'),
('~', 'Changed'),
('-', 'Deleted'),
)),
'history_user': models.ForeignKey(
'accounts.User',
null=True,
on_delete=models.SET_NULL,
related_name='+'
),
})
# Convert field to point to historical model
fields = self.copy_fields(model)
attrs.update(fields)
return type(
str('Historical%s' % model._meta.object_name),
(models.Model,),
attrs
)

View File

@@ -0,0 +1,49 @@
from django.db import models
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
from typing import List, Type
def get_trackable_fields(model_class: Type[models.Model]) -> List[models.Field]:
"""Get fields that should be tracked in history."""
if getattr(model_class, '_is_historical_model', False):
# For historical models, only return core history fields
return [
models.BigAutoField(name='id', primary_key=True),
models.DateTimeField(name='history_date'),
models.CharField(name='history_change_reason', max_length=100, null=True),
models.CharField(name='history_type', max_length=1),
models.ForeignKey(
to=settings.AUTH_USER_MODEL,
name='history_user',
null=True,
on_delete=models.SET_NULL
)
]
trackable_fields = []
excluded_fields = {
'comment_threads', 'comments', 'photos', 'reviews',
'thread', 'content_type', 'object_id', 'content_object'
}
for field in model_class._meta.get_fields():
# Skip fields we don't want to track
if any([
isinstance(field, (GenericRelation, GenericForeignKey)),
field.name in excluded_fields,
field.is_relation and hasattr(field.remote_field.model, '_meta') and
'commentthread' in field.remote_field.model._meta.model_name.lower()
]):
continue
trackable_fields.append(field)
return trackable_fields
class HistoricalFieldsMixin:
"""Mixin that controls which fields are copied to historical models."""
@classmethod
def get_fields_to_track(cls) -> List[models.Field]:
"""Get fields that should be tracked in history."""
return get_trackable_fields(cls)

View File

@@ -1,10 +1,13 @@
from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.db.models.fields.related import RelatedField
from django.contrib.auth import get_user_model
from simple_history.models import HistoricalRecords
from .mixins import HistoricalChangeMixin
from typing import Any, Type, TypeVar, cast, Optional
from .historical_fields import HistoricalFieldsMixin
from typing import Any, Type, TypeVar, cast, Optional, List
from django.db.models import QuerySet
from django.core.exceptions import ValidationError
from django.utils import timezone
@@ -13,13 +16,28 @@ T = TypeVar('T', bound=models.Model)
User = get_user_model()
class HistoricalModel(models.Model):
class HistoricalModel(models.Model, HistoricalFieldsMixin):
"""Abstract base class for models with history tracking"""
id = models.BigAutoField(primary_key=True)
history: HistoricalRecords = HistoricalRecords(
@classmethod
def __init_subclass__(cls, **kwargs):
"""Initialize subclass with proper configuration."""
super().__init_subclass__(**kwargs)
# Mark historical models
if cls.__name__.startswith('Historical'):
cls._is_historical_model = True
# Remove any inherited generic relations
for field in list(cls._meta.private_fields):
if isinstance(field, GenericRelation):
cls._meta.private_fields.remove(field)
else:
cls._is_historical_model = False
history = HistoricalRecords(
inherit=True,
bases=(HistoricalChangeMixin,),
excluded_fields=['comments', 'photos', 'reviews'] # Exclude all generic relations
bases=[HistoricalChangeMixin],
excluded_fields=['comments', 'comment_threads', 'photos', 'reviews'],
use_base_model_db=True # Use base model's db
)
class Meta: