[WEB-5285] feat: add ChangeTrackerMixin to track model field changes and original values #8145
This commit is contained in:
parent
1eaa48c95c
commit
f510020daa
1 changed files with 108 additions and 0 deletions
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Type imports
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
@ -80,3 +83,108 @@ class AuditModel(TimeAuditModel, UserAuditModel, SoftDeleteModel):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue