Created
December 6, 2010 19:15
-
-
Save dcramer/730765 to your computer and use it in GitHub Desktop.
Tracking changes on properties in Django
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.db.models.signals import post_init | |
def track_data(*fields): | |
""" | |
Tracks property changes on a model instance. | |
The changed list of properties is refreshed on model initialization | |
and save. | |
>>> @track_data('name') | |
>>> class Post(models.Model): | |
>>> name = models.CharField(...) | |
>>> | |
>>> @classmethod | |
>>> def post_save(cls, sender, instance, created, **kwargs): | |
>>> if instance.has_changed('name'): | |
>>> print "Hooray!" | |
""" | |
UNSAVED = dict() | |
def _store(self): | |
"Updates a local copy of attributes values" | |
if self.id: | |
self.__data = dict((f, getattr(self, f)) for f in fields) | |
else: | |
self.__data = UNSAVED | |
def inner(cls): | |
# contains a local copy of the previous values of attributes | |
cls.__data = {} | |
def has_changed(self, field): | |
"Returns ``True`` if ``field`` has changed since initialization." | |
if self.__data is UNSAVED: | |
return False | |
return self.__data.get(field) != getattr(self, field) | |
cls.has_changed = has_changed | |
def old_value(self, field): | |
"Returns the previous value of ``field``" | |
return self.__data.get(field) | |
cls.old_value = old_value | |
def whats_changed(self): | |
"Returns a list of changed attributes." | |
changed = {} | |
if self.__data is UNSAVED: | |
return changed | |
for k, v in self.__data.iteritems(): | |
if v != getattr(self, k): | |
changed[k] = v | |
return changed | |
cls.whats_changed = whats_changed | |
# Ensure we are updating local attributes on model init | |
def _post_init(sender, instance, **kwargs): | |
_store(instance) | |
post_init.connect(_post_init, sender=cls, weak=False) | |
# Ensure we are updating local attributes on model save | |
def save(self, *args, **kwargs): | |
save._original(self, *args, **kwargs) | |
_store(self) | |
save._original = cls.save | |
cls.save = save | |
return cls | |
return inner |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@bradreardon Thanks for the hint! Was running into lots of
RecursionError
when trying to upgrade Django from 2.2 to 3.2. Ignoring deferred fields inside the_store()
function helped, but some were still left. I am still trying to figure out why.EDIT: This ticket seems relevant https://code.djangoproject.com/ticket/31435 as does this commit that is mentioned in there django/django@f110de5.
As a temporary workaround, to get into a workable state, I used this decorator to stop infinite recursions (applied on top of the
_store()
function):UPDATE: I think I have found a slightly better solution at least for our setup. The problem in general seems to be that upon deletion of a model instance, Django may create a QuerySet of related objects using
.only()
aka with deferred fields. Evaluation that inside a boolean context will fetch that QuerySet from the database, triggering an infinite recursion of model instance initialization andtrack_data()
calls. This can however be detected by looking at the args of the__init__()
function, if those containDeferred
values, we likely don't want to try and store the current state (as on deletion we don't care about field changes anymore).My approach now looks like this (overriding
__init__()
instead of handling thepost_init
signal):