[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
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue