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}"