From 5ba8d2b4d72819f154a11f297796e6a2bb7172bf Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Mon, 13 Jan 2014 23:46:32 -0500 Subject: [PATCH] Added Many-to-Many Support See #16 --- .gitignore | 1 + simple_history/models.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 01dc39749..119343552 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test_files/ /.ve /.project /.pydevproject +.idea/ diff --git a/simple_history/models.py b/simple_history/models.py index 7671f0a68..f1e0574ee 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -7,6 +7,7 @@ from django.contrib import admin from django.utils import importlib from .manager import HistoryDescriptor +from simple_history import register try: basestring @@ -35,14 +36,16 @@ def python_2_unicode_compatible(klass): class HistoricalRecords(object): - def __init__(self, verbose_name=None): + def __init__(self, verbose_name=None, m2m_fields=None): self.user_set_verbose_name = verbose_name + self.m2m_fields = m2m_fields def contribute_to_class(self, cls, name): self.manager_name = name self.module = cls.__module__ models.signals.class_prepared.connect(self.finalize, sender=cls) self.add_extra_methods(cls) + self.setup_m2m_history(cls) def add_extra_methods(self, cls): def save_without_historical_record(self, *args, **kwargs): @@ -60,6 +63,17 @@ def save_without_historical_record(self, *args, **kwargs): setattr(cls, 'save_without_historical_record', save_without_historical_record) + 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) + def finalize(self, sender, **kwargs): history_model = self.create_history_model(sender) module = importlib.import_module(self.module) @@ -71,6 +85,7 @@ 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) @@ -177,6 +192,27 @@ def post_save(self, instance, created, **kwargs): def post_delete(self, instance, **kwargs): self.create_historical_record(instance, '-') + 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): + if field_value.field.related.parent_model == kwargs['model']: + target_field_name = field_name + elif field_value.field.related.parent_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 create_historical_record(self, instance, type): history_user = getattr(instance, '_history_user', None) manager = getattr(instance, self.manager_name)