from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.conf import settings from typing import Any, Dict, Optional from django.db.models import QuerySet class DiffMixin: """Mixin to add diffing capabilities to models""" def get_prev_record(self) -> Optional[Any]: """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) -> Dict: """Compare this record against the previous one""" prev_record = self.get_prev_record() if not prev_record: return {} skip_fields = { "pgh_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "pgh_context_id", "_state", "created_at", "updated_at", } changes = {} for field, value in self.__dict__.items(): # Skip internal fields and those we don't want to track if field.startswith("_") or field in skip_fields or field.endswith("_id"): continue try: old_value = getattr(prev_record, field) new_value = value if old_value != new_value: changes[field] = { "old": (str(old_value) if old_value is not None else "None"), "new": (str(new_value) if new_value is not None else "None"), } except AttributeError: continue return changes class TrackedModel(models.Model): """Abstract base class for models that need history tracking""" created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True def get_history(self) -> QuerySet: """Get all history records for this instance in chronological order""" event_model = self.events.model # pghistory provides this automatically if event_model: return event_model.objects.filter(pgh_obj_id=self.pk).order_by( "-pgh_created_at" ) return self.__class__.objects.none() 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) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="historical_slugs", ) 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}"