# history_tracking/mixins.py from django.db import models from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation class HistoricalChangeMixin(models.Model): """Mixin for historical models to track changes""" comments = GenericRelation('CommentThread', related_query_name='historical_record') id = models.BigIntegerField(db_index=True, auto_created=True, blank=True) history_date = models.DateTimeField() history_id = models.AutoField(primary_key=True) history_type = models.CharField(max_length=1) history_user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, related_name='+' ) history_change_reason = models.CharField(max_length=100, null=True) class Meta: abstract = True ordering = ['-history_date', '-history_id'] @property def prev_record(self): """Get the previous record for this instance""" try: return self.__class__.objects.filter( history_date__lt=self.history_date, id=self.id ).order_by('-history_date').first() except (AttributeError, TypeError): return None @property def diff_against_previous(self): """Get enhanced diff with syntax highlighting and metadata""" prev_record = self.prev_record if not prev_record: return {} changes = {} for field in self.__dict__: if field not in [ "history_date", "history_id", "history_type", "history_user_id", "history_change_reason", "history_type", "id", "_state", "_history_user_cache" ] and not field.startswith("_"): try: old_value = getattr(prev_record, field) new_value = getattr(self, field) if old_value != new_value: field_type = self._meta.get_field(field).get_internal_type() syntax_type = self._get_syntax_type(field_type) changes[field] = { "old": str(old_value), "new": str(new_value), "syntax_type": syntax_type, "metadata": { "field_type": field_type, "comment_anchor_id": f"{self.history_id}_{field}", "line_numbers": self._compute_line_numbers(old_value, new_value) } } except AttributeError: continue return changes def _get_syntax_type(self, field_type): """Map Django field types to syntax highlighting types""" syntax_map = { 'TextField': 'text', 'JSONField': 'json', 'FileField': 'path', 'ImageField': 'path', 'URLField': 'url', 'EmailField': 'email', 'CodeField': 'python' # Custom field type for code } return syntax_map.get(field_type, 'text') def _compute_line_numbers(self, old_value, new_value): """Compute line numbers for diff navigation""" old_lines = str(old_value).count('\n') + 1 new_lines = str(new_value).count('\n') + 1 return { "old": list(range(1, old_lines + 1)), "new": list(range(1, new_lines + 1)) } def get_structured_diff(self, other_version=None): """Get structured diff between two versions with enhanced metadata""" compare_to = other_version or self.prev_record if not compare_to: return None diff_data = self.diff_against_previous return { "changes": diff_data, "metadata": { "timestamp": self.history_date.isoformat(), "user": self.history_user_display, "change_type": self.history_type, "reason": self.history_change_reason, "performance": { "computation_time": None # To be filled by frontend } }, "navigation": { "next_id": None, # To be filled by frontend "prev_id": None, # To be filled by frontend "current_position": None # To be filled by frontend } } @property def history_user_display(self): """Get a display name for the history user""" if hasattr(self, 'history_user') and self.history_user: return str(self.history_user) return None def get_instance(self): """Get the model instance this history record represents""" try: return self.__class__.objects.get(id=self.id) except self.__class__.DoesNotExist: return None