Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django model changes migration #12

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e795c6a
Make model changes work with mongo engine.
myprasanna Jul 15, 2016
004d7f5
Ignore the compiled egg.
myprasanna Jul 26, 2016
c220c0c
Use data instead of attributes.
myprasanna Aug 2, 2016
10ee189
Ignore pyc files
aswin Sep 28, 2016
685bd05
Merge pull request #1 from Rippling/gitignore
aswin Sep 28, 2016
5fd8700
Save modal state before or after initializing the fields based on the…
aswin Oct 8, 2016
1d44380
Merge pull request #2 from Rippling/idchanges
Oct 8, 2016
db641b8
deep copy for on_cache current_state snapshotting
nulpoet Nov 20, 2016
5d5ad61
Speedup changes. 20x.
myprasanna Nov 24, 2016
f4e1370
Work well with reload.
myprasanna Feb 4, 2017
7c90f00
Work well with reload.
myprasanna Feb 4, 2017
4db08ff
Send changes to post_change hooks
mohitm86 Feb 22, 2017
708c0b3
Merge pull request #3 from Rippling/post_change
Feb 22, 2017
a0cf93d
Add kwargs
sauravshah Mar 20, 2017
96722d9
Merge pull request #4 from Rippling/kwargs
sauravshah Mar 20, 2017
3c62fda
Add type check for DocumentProxy
sauravshah Jul 2, 2017
51c852e
skip saving states in Historical models.
myprasanna Oct 4, 2017
72a4e6b
kill django model changes.
myprasanna Oct 5, 2017
f232566
Complete re-write.
myprasanna Oct 5, 2017
1b1320f
remove was not now check
myprasanna Oct 5, 2017
a6e83e5
register signals only once.
myprasanna Oct 5, 2017
01e023e
Move signal registry to first save.
myprasanna Oct 5, 2017
dac6db6
Fix for inherited classes.
myprasanna Oct 5, 2017
4ba7045
changes() should consider force_update_fields
sauravshah Oct 22, 2019
cf13ccc
module migration
ParmarMiral Feb 10, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.egg-info
*.pyc

4 changes: 2 additions & 2 deletions django_model_changes/__init__.py
Original file line number Diff line number Diff line change
@@ -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
239 changes: 53 additions & 186 deletions django_model_changes/changes.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
1 change: 1 addition & 0 deletions django_model_changes/signals.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import absolute_import
from django.dispatch import Signal


Expand Down
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions runtests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
from __future__ import absolute_import
import sys
import os
from os.path import dirname, abspath
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import absolute_import
import os
from setuptools import setup, find_packages

Expand Down
1 change: 1 addition & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import absolute_import
from django.db import models
from django_model_changes.changes import ChangesMixin

Expand Down
1 change: 1 addition & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import absolute_import
from django.test import TestCase

from .models import User, Article
Expand Down