Skip to content

Commit

Permalink
add Many-To-Many support to 1.6.3
Browse files Browse the repository at this point in the history
  • Loading branch information
Kenneth Feng committed Apr 28, 2016
1 parent 6e09bdc commit 938e57d
Showing 1 changed file with 47 additions and 2 deletions.
49 changes: 47 additions & 2 deletions simple_history/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
[], ["^simple_history.models.CustomForeignKeyField"])

from .manager import HistoryDescriptor
from . import register

registered_models = {}

Expand All @@ -44,9 +45,10 @@ class HistoricalRecords(object):
thread = threading.local()

def __init__(self, verbose_name=None, bases=(models.Model,),
user_related_name='+'):
user_related_name='+', m2m_fields=None):
self.user_set_verbose_name = verbose_name
self.user_related_name = user_related_name
self.m2m_fields = m2m_fields
try:
if isinstance(bases, six.string_types):
raise TypeError
Expand All @@ -57,9 +59,49 @@ def __init__(self, verbose_name=None, bases=(models.Model,),
def contribute_to_class(self, cls, name):
self.manager_name = name
self.module = cls.__module__
models.signals.class_prepared.connect(self.finalize, sender=cls)
models.signals.class_prepared.connect(self.finalize, sender=cls, weak=False)
self.add_extra_methods(cls)

def setup_m2m_history(self, cls):
m2m_history_fields = self.m2m_fields
if m2m_history_fields:
assert isinstance(m2m_history_fields, list) or isinstance(m2m_history_fields, tuple), \
'm2m_history_fields must be a list or tuple'
for field_name in m2m_history_fields:
field = getattr(cls, field_name).field
assert isinstance(field, models.fields.related.ManyToManyField), \
'%s must be a ManyToManyField' % field_name
if not sum([isinstance(item, HistoricalRecords) for item in field.rel.through.__dict__.values()]):
field.rel.through.history = HistoricalRecords()
register(field.rel.through, app=self.module.split('.')[0])

def m2m_changed(self, action, instance, sender, **kwargs):
source_field_name, target_field_name = None, None
for field_name, field_value in sender.__dict__.items():
if isinstance(field_value, models.fields.related.ReverseSingleRelatedObjectDescriptor):
try:
root_model = field_value.field.related.parent_model
except AttributeError:
root_model = field_value.field.related.model

if root_model == kwargs['model']:
target_field_name = field_name
elif root_model == type(instance):
source_field_name = field_name

items = sender.objects.filter(**{source_field_name: instance})
if kwargs['pk_set']:
items = items.filter(**{target_field_name + '__id__in': kwargs['pk_set']})
for item in items:
if action == 'post_add':
if hasattr(item, 'skip_history_when_saving'):
return
self.create_historical_record(item, '+')
elif action == 'pre_remove':
self.create_historical_record(item, '-')
elif action == 'pre_clear':
self.create_historical_record(item, '-')

def add_extra_methods(self, cls):
def save_without_historical_record(self, *args, **kwargs):
"""
Expand All @@ -77,6 +119,7 @@ def save_without_historical_record(self, *args, **kwargs):
save_without_historical_record)

def finalize(self, sender, **kwargs):
self.setup_m2m_history(sender)
history_model = self.create_history_model(sender)
module = importlib.import_module(self.module)
setattr(module, history_model.__name__, history_model)
Expand All @@ -87,6 +130,8 @@ def finalize(self, sender, **kwargs):
weak=False)
models.signals.post_delete.connect(self.post_delete, sender=sender,
weak=False)
models.signals.m2m_changed.connect(self.m2m_changed, sender=sender,
weak=False)

descriptor = HistoryDescriptor(history_model)
setattr(sender, self.manager_name, descriptor)
Expand Down

2 comments on commit 938e57d

@jamesmgg
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @kennethzfeng could we update the base version on this branch for this to work?

@kennethzfeng
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamesmgg I don't plan on updating the base version for this. It shouldn't be too hard to make this work for Django 1.9+

Please sign in to comment.