Skip to content

Instantly share code, notes, and snippets.

@llk89
Forked from dcramer/track_data.py
Last active March 14, 2019 07:54
Show Gist options
  • Select an option

  • Save llk89/f4711bc879a1aff3cfbca98e1de11afd to your computer and use it in GitHub Desktop.

Select an option

Save llk89/f4711bc879a1aff3cfbca98e1de11afd to your computer and use it in GitHub Desktop.
Tracking changes on properties in Django
from django.db.models.signals import post_init
def _iter_or_none(o):
try:
return iter(o)
except TypeError:
return None
def track_data(*fields, new_instance_indicator=None):
"""
Tracks property changes on a model instance.
The changed list of properties is refreshed on model initialization
and save, i.e. if model is not saved
>>> @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!"
Based on https://gist.github.com/dcramer/730765
Adapted to multi table inheritance. For use, you must manually
specify the pk field of the root ancestor. This decorator will not
figure it out automatically.
:param new_instance_indicator: This one can be
1. None, decorator will use pk as indicator. Does not work with
multi table inheritance.
2. str or list[str], decorator will try to find a non null field with given name.
If at least one is found, this will not be considered a new instance
3. callable, if above does not suffice... well.
"""
UNSAVED = dict()
def _store(self):
"""Updates a local copy of attributes values"""
if self.is_new_instance():
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_attrs_op(*names):
"""Returns ``True`` if this instance is created from scratch"""
def inner(self):
return not any(hasattr(self, name) and getattr(self, name) is not None for name in names)
return inner
if new_instance_indicator is None:
cls.is_new_instance = has_attrs_op(cls._meta.pk.name)
elif callable(new_instance_indicator):
cls.is_new_instance = new_instance_indicator
elif isinstance(new_instance_indicator, str):
cls.is_new_instance = has_attrs_op(new_instance_indicator)
elif _iter_or_none(new_instance_indicator):
cls.is_new_instance = has_attrs_op(*new_instance_indicator)
else:
raise TypeError("Unrecognized type for new_instance_indicator: {}".format(type(new_instance_indicator)))
def has_changed(self, field):
"""Returns ``True`` if ``field`` has changed since initialization."""
if self.is_new_instance():
return self
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 None if self.is_new_instance() else self.__data.get(field)
cls.old_value = old_value
def whats_changed(self):
"""Returns a list of changed attributes."""
changed = {}
if self.is_new_instance():
return changed
for k, v in self.__data.items():
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