diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e5c320 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.egg-info +*.pyc + diff --git a/django_model_changes/__init__.py b/django_model_changes/__init__.py index b6c912c..b51cbb4 100644 --- a/django_model_changes/__init__.py +++ b/django_model_changes/__init__.py @@ -1,2 +1,2 @@ -from changes import ChangesMixin -from signals import post_change +from django_model_changes.changes import ChangesMixin +from django_model_changes.signals import post_change diff --git a/django_model_changes/changes.py b/django_model_changes/changes.py index 3d0d595..c21c24a 100644 --- a/django_model_changes/changes.py +++ b/django_model_changes/changes.py @@ -1,4 +1,7 @@ -from django.db.models import signals +from __future__ import absolute_import +import copy +from mongoengine import signals +from mongoengine.base.proxy import DocumentProxy from .signals import post_change @@ -7,200 +10,64 @@ class ChangesMixin(object): - """ - ChangesMixin keeps track of changes for model instances. - It allows you to retrieve the following states from an instance: - - 1. current_state() - The current state of the instance. - 2. previous_state() - The state of the instance **after** it was created, saved - or deleted the last time. - 3. old_state() - The previous previous_state(), i.e. the state of the - instance **before** it was created, saved or deleted the - last time. - - It also provides convenience methods to get changes between states: - - 1. changes() - Changes from previous_state to current_state. - 2. previous_changes() - Changes from old_state to previous_state. - 3. old_changes() - Changes from old_state to current_state. - - And the following methods to determine if an instance was/is persisted in - the database: - - 1. was_persisted() - Was the instance persisted in its old state. - 2. is_persisted() - Is the instance is_persisted in its current state. - - This schematic tries to illustrate how these methods relate to - each other:: - - - after create/save/delete after save/delete now - | | | - .-----------------------------------.----------------------------------. - |\ |\ |\ - | \ | \ | \ - | old_state() | previous_state() | current_state() - | | | - |-----------------------------------|----------------------------------| - | previous_changes() (prev - old) | changes() (cur - prev) | - |-----------------------------------|----------------------------------| - | old_changes() (cur - old) | - .----------------------------------------------------------------------. - \ \ - \ \ - was_persisted() is_persisted() - - """ - - def __init__(self, *args, **kwargs): - super(ChangesMixin, self).__init__(*args, **kwargs) - - self._states = [] - self._save_state(new_instance=True) - - signals.post_save.connect( - _post_save, sender=self.__class__, - dispatch_uid='django-changes-%s' % self.__class__.__name__ - ) - signals.post_delete.connect( - _post_delete, sender=self.__class__, - dispatch_uid='django-changes-%s' % self.__class__.__name__ - ) - - def _save_state(self, new_instance=False, event_type='save'): + def save(self, *args, **kwargs): + self.__class__.register_signals() + return super(ChangesMixin, self).save(*args, **kwargs) + + @classmethod + def register_signals(cls): + key = ('changes_signal_registered_{}'.format(cls.__name__)) + if not getattr(cls, key, False): + setattr(cls, key, True) + signals.post_save.connect( + _post_save, sender=cls, + ) + signals.post_delete.connect( + _post_delete, sender=cls, + ) + + def _save_state(self, new_instance=False, event_type='save', **kwargs): + if "Historical" in self.__class__.__name__: + return + # Pipe the pk on deletes so that a correct snapshot of the current # state can be taken. if event_type == DELETE: self.pk = None - # Save current state. - self._states.append(self.current_state()) - - # Drop the previous old state - # _states == [previous old state, old state, previous state] - # ^^^^^^^^^^^^^^^^^^ - if len(self._states) > 2: - self._states.pop(0) - # Send post_change signal unless this is a new instance if not new_instance: - post_change.send(sender=self.__class__, instance=self) - - def current_state(self): - """ - Returns a ``field -> value`` dict of the current state of the instance. - """ - field_names = set() - [field_names.add(f.name) for f in self._meta.local_fields] - [field_names.add(f.attname) for f in self._meta.local_fields] - return dict([(field_name, getattr(self, field_name)) for field_name in field_names]) - - def previous_state(self): - """ - Returns a ``field -> value`` dict of the state of the instance after it - was created, saved or deleted the previous time. - """ - if len(self._states) > 1: - return self._states[1] - else: - return self._states[0] - - def old_state(self): - """ - Returns a ``field -> value`` dict of the state of the instance after - it was created, saved or deleted the previous previous time. Returns - the previous state if there is no previous previous state. - """ - return self._states[0] - - def _changes(self, other, current): - return dict([(key, (was, current[key])) for key, was in other.iteritems() if was != current[key]]) - + post_change.send(sender=self.__class__, instance=self, + changes=self._calculate_changes(**kwargs), + **kwargs) + + def _calculate_changes(self, created=False, _changed_fields=None, _original_values=None, **kwargs): + if _changed_fields is None: + _changed_fields = getattr(self, '_changed_fields', []) + if _original_values is None: + _original_values = getattr(self, '_original_values', {}) + + _force_changed_fields = getattr(self, '_force_changed_fields', set()) + + if created: + _changed_fields = list(self._data.keys()) + + res = {} + for field in set(_changed_fields) | _force_changed_fields: + if field not in ["_id"]: + was = _original_values.get(field, None) + now = getattr(self, field, None) + res[field] = (was, now) + + return res + def changes(self): - """ - Returns a ``field -> (previous value, current value)`` dict of changes - from the previous state to the current state. - """ - return self._changes(self.previous_state(), self.current_state()) - - def old_changes(self): - """ - Returns a ``field -> (previous value, current value)`` dict of changes - from the old state to the current state. - """ - return self._changes(self.old_state(), self.current_state()) - - def previous_changes(self): - """ - Returns a ``field -> (previous value, current value)`` dict of changes - from the old state to the previous state. - """ - return self._changes(self.old_state(), self.previous_state()) - - def was_persisted(self): - """ - Returns true if the instance was persisted (saved) in its old - state. - - Examples:: - - >>> user = User() - >>> user.save() - >>> user.was_persisted() - False - - >>> user = User.objects.get(pk=1) - >>> user.delete() - >>> user.was_persisted() - True - """ - pk_name = self._meta.pk.name - return bool(self.old_state()[pk_name]) - - def is_persisted(self): - """ - Returns true if the instance is persisted (saved) in its current - state. - - Examples: - - >>> user = User() - >>> user.save() - >>> user.is_persisted() - True - - >>> user = User.objects.get(pk=1) - >>> user.delete() - >>> user.is_persisted() - False - """ - return bool(self.pk) - - def old_instance(self): - """ - Returns an instance of this model in its old state. - """ - return self.__class__(**self.old_state()) - - def previous_instance(self): - """ - Returns an instance of this model in its previous state. - """ - return self.__class__(**self.previous_state()) - + return self._calculate_changes() -def _post_save(sender, instance, **kwargs): - instance._save_state(new_instance=False, event_type=SAVE) +def _post_save(sender, **kwargs): + kwargs['document']._save_state(new_instance=False, event_type=SAVE, **kwargs) -def _post_delete(sender, instance, **kwargs): - instance._save_state(new_instance=False, event_type=DELETE) +def _post_delete(sender, **kwargs): + kwargs['document']._save_state(new_instance=False, event_type=DELETE, **kwargs) diff --git a/django_model_changes/signals.py b/django_model_changes/signals.py index a467746..ab51fa8 100644 --- a/django_model_changes/signals.py +++ b/django_model_changes/signals.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from django.dispatch import Signal diff --git a/docs/source/conf.py b/docs/source/conf.py index 8f03199..80ade91 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,6 +11,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. +from __future__ import absolute_import import sys, os # If extensions (or django-model-changes to document with autodoc) are in another directory, diff --git a/runtests.py b/runtests.py index 180f4e7..b43f040 100644 --- a/runtests.py +++ b/runtests.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import absolute_import import sys import os from os.path import dirname, abspath diff --git a/setup.py b/setup.py index 29bb5d3..c71ec5d 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import os from setuptools import setup, find_packages diff --git a/tests/models.py b/tests/models.py index a30f453..732c6b9 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from django.db import models from django_model_changes.changes import ChangesMixin diff --git a/tests/tests.py b/tests/tests.py index 12806e5..5204fea 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from django.test import TestCase from .models import User, Article