-
-
Save llk89/f4711bc879a1aff3cfbca98e1de11afd 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 _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