# history_tracking/models.py from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.auth import get_user_model from simple_history.models import HistoricalRecords from .mixins import HistoricalChangeMixin from typing import Any, Type, TypeVar, cast from django.db.models import QuerySet T = TypeVar('T', bound=models.Model) class DiffMixin: """Mixin to add diffing capabilities to pghistory events""" def get_prev_record(self): """Get the previous record for this instance""" try: return type(self).objects.filter( pgh_created_at__lt=self.pgh_created_at, pgh_obj_id=self.pgh_obj_id ).order_by('-pgh_created_at').first() except (AttributeError, TypeError): return None def diff_against_previous(self): """Compare this record against the previous one""" prev_record = self.get_prev_record() if not prev_record: return {} changes = {} skip_fields = { 'pgh_id', 'pgh_created_at', 'pgh_label', 'pgh_obj_id', 'pgh_context_id', '_state' } for field in self.__dict__: if field not in skip_fields and not field.startswith('_'): try: old_value = getattr(prev_record, field) new_value = getattr(self, field) if old_value != new_value: changes[field] = {"old": str(old_value), "new": str(new_value)} except AttributeError: continue return changes class HistoricalModel(models.Model): """ Legacy abstract base class for models with history tracking. Use TrackedModel for new implementations. """ id = models.BigAutoField(primary_key=True) history: HistoricalRecords = HistoricalRecords( inherit=True, bases=(HistoricalChangeMixin,) ) class Meta: abstract = True @property def _history_model(self) -> Type[T]: """Get the history model class""" return cast(Type[T], self.history.model) # type: ignore def get_history(self) -> QuerySet: """Get all history records for this instance""" model = self._history_model return model.objects.filter(id=self.pk).order_by('-history_date') class TrackedModel(models.Model): """Abstract base class for models with pghistory tracking. The @pghistory.track() decorator should be applied to concrete models that inherit from this class. """ id = models.BigAutoField(primary_key=True) class Meta: abstract = True def get_history(self) -> QuerySet: """Get all history records for this instance in chronological order""" history_model = self.get_history_model() return history_model.objects.filter( pgh_obj_id=self.pk ).order_by('-pgh_created_at') def get_history_model(self) -> Type[Any]: """Get the pghistory model for this instance""" return self.pgh_obj_event_model class HistoricalSlug(models.Model): """Track historical slugs for models""" content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') slug = models.SlugField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('content_type', 'slug') indexes = [ models.Index(fields=['content_type', 'object_id']), models.Index(fields=['slug']), ] def __str__(self) -> str: return f"{self.content_type} - {self.object_id} - {self.slug}"