[WEB-5285] feat: add ChangeTrackerMixin to track model field changes and original values #8145

This commit is contained in:
Dheeraj Kumar Ketireddy 2025-11-20 14:36:55 +05:30 committed by GitHub
parent 1eaa48c95c
commit f510020daa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,3 +1,6 @@
# Type imports
from typing import Any
# Django imports
from django.db import models
from django.utils import timezone
@ -80,3 +83,108 @@ class AuditModel(TimeAuditModel, UserAuditModel, SoftDeleteModel):
class Meta:
abstract = True
class ChangeTrackerMixin:
"""
A mixin to track changes in model fields between initialization and save.
This mixin captures the initial state of model fields when the instance is
created and provides utilities to detect which fields have changed.
Usage:
To track specific fields, define a TRACKED_FIELDS list on your model:
class MyModel(ChangeTrackerMixin, models.Model):
TRACKED_FIELDS = ['field1', 'field2', 'field3']
field1 = models.CharField(max_length=100)
field2 = models.IntegerField()
field3 = models.BooleanField()
If TRACKED_FIELDS is not defined, all non-deferred fields will be tracked.
Properties:
changed_fields: A list of field names that have changed since initialization.
old_values: A dictionary mapping field names to their original values.
Methods:
has_changed(field_name): Check if a specific field has changed.
Notes:
- Deferred fields (from .defer() or .only()) are automatically excluded
from tracking to avoid triggering database queries.
- Field values are captured in __init__, so changes are tracked relative
to the initial state when the instance was loaded from the database.
"""
_original_values: dict[str, Any]
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._original_values = {}
self._track_fields()
def _track_fields(self) -> None:
"""
Capture the initial values of fields to track.
This method stores the current values of fields that should be tracked.
If TRACKED_FIELDS is defined on the model, only those fields are tracked.
Otherwise, all non-deferred fields are tracked. Deferred fields are
automatically excluded to prevent unnecessary database queries.
"""
deferred_fields = self.get_deferred_fields()
tracked_fields = getattr(self, "TRACKED_FIELDS", None)
if tracked_fields:
for field in tracked_fields:
if field not in deferred_fields:
self._original_values[field] = getattr(self, field)
else:
for field in self._meta.fields:
if field.attname not in deferred_fields:
self._original_values[field.attname] = getattr(self, field.attname)
def has_changed(self, field_name: str) -> bool:
"""
Check if a specific field has changed since initialization.
Args:
field_name (str): The name of the field to check.
Returns:
bool: True if the field has changed, False otherwise. Returns False
if the field was not being tracked or is deferred.
"""
if field_name not in self._original_values:
return False
return self._original_values[field_name] != getattr(self, field_name)
@property
def changed_fields(self) -> list[str]:
"""
Get a list of all fields that have changed since initialization.
Returns:
list[str]: A list of field names that have different values than
when the instance was initialized. Returns an empty list
if no fields have changed.
"""
changed = []
for field, old_val in self._original_values.items():
new_val = getattr(self, field)
if old_val != new_val:
changed.append(field)
return changed
@property
def old_values(self) -> dict[str, Any]:
"""
Get a dictionary of the original field values from initialization.
Returns:
dict: A dictionary mapping field names to their original values
as they were when the instance was initialized. Only includes
fields that are being tracked (either via TRACKED_FIELDS or
all non-deferred fields).
"""
return self._original_values