Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Tracking changes on properties in Django
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
@DXist

This comment has been minimized.

Copy link

@DXist DXist commented Mar 20, 2012

It would be great to add to the decorator model inheritance support:

  • no need to specify parent fields in child's decorator
  • track fields only once in child decorator
@trent

This comment has been minimized.

Copy link

@trent trent commented Aug 10, 2012

@jlachowski

This comment has been minimized.

Copy link

@jlachowski jlachowski commented Jul 25, 2013

I had some problems with it using @classmethod and post_save as described above.
I got it to work by defining it as a non class function and registering it as a post_save receiver on my model.

Also the line #52:

changed[k] = v

should be:

changed[k] = getattr(self, k)

in order to get the new value in the instance.whats_changed() dictionary. The old one is available via instance.old_value(...).

@andreip

This comment has been minimized.

Copy link

@andreip andreip commented Sep 8, 2016

in order to get the new value in the instance.whats_changed() dictionary. The old one is available via instance.old_value(...).

And the new value is available through instance.field directly. So all you need, actually, are the key values of what changed. Anyhow, depends on how you use the method.

@bradreardon

This comment has been minimized.

Copy link

@bradreardon bradreardon commented Dec 9, 2020

For anyone finding this years later, I ran into an issue where infinite recursion would occur when deleting instances of "tracked" models in the Django admin. I believe this was due to the fact that the Django admin uses deferred fields in this case, which causes an infinite loop during the post_init signal.

I've fixed that in my fork of the gist here by modifying _store to ignore deferred fields: https://gist.github.com/bradreardon/0427aec70733494976ff8b64c73a60ed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment