32
32
33
33
from . import exceptions
34
34
from .manager import SIMPLE_HISTORY_REVERSE_ATTR_NAME , HistoryDescriptor
35
- from .signals import post_create_historical_record , pre_create_historical_record
35
+ from .signals import (
36
+ post_create_historical_m2m_records ,
37
+ post_create_historical_record ,
38
+ pre_create_historical_m2m_records ,
39
+ pre_create_historical_record ,
40
+ )
36
41
from .utils import get_change_reason_from_object
37
42
38
43
try :
@@ -94,6 +99,8 @@ def __init__(
94
99
no_db_index = list (),
95
100
excluded_field_kwargs = None ,
96
101
m2m_fields = (),
102
+ m2m_fields_model_field_name = "_history_m2m_fields" ,
103
+ m2m_bases = (models .Model ,),
97
104
):
98
105
self .user_set_verbose_name = verbose_name
99
106
self .user_set_verbose_name_plural = verbose_name_plural
@@ -114,6 +121,7 @@ def __init__(
114
121
self .related_name = related_name
115
122
self .use_base_model_db = use_base_model_db
116
123
self .m2m_fields = m2m_fields
124
+ self .m2m_fields_model_field_name = m2m_fields_model_field_name
117
125
118
126
if isinstance (no_db_index , str ):
119
127
no_db_index = [no_db_index ]
@@ -132,6 +140,12 @@ def __init__(
132
140
self .bases = (HistoricalChanges ,) + tuple (bases )
133
141
except TypeError :
134
142
raise TypeError ("The `bases` option must be a list or a tuple." )
143
+ try :
144
+ if isinstance (m2m_bases , str ):
145
+ raise TypeError
146
+ self .m2m_bases = (HistoricalChanges ,) + tuple (m2m_bases )
147
+ except TypeError :
148
+ raise TypeError ("The `m2m_bases` option must be a list or a tuple." )
135
149
136
150
def contribute_to_class (self , cls , name ):
137
151
self .manager_name = name
@@ -189,7 +203,10 @@ def finalize(self, sender, **kwargs):
189
203
# so the signal handlers can't use weak references.
190
204
models .signals .post_save .connect (self .post_save , sender = sender , weak = False )
191
205
models .signals .post_delete .connect (self .post_delete , sender = sender , weak = False )
192
- for field in self .m2m_fields :
206
+
207
+ m2m_fields = self .get_m2m_fields_from_model (sender )
208
+
209
+ for field in m2m_fields :
193
210
m2m_changed .connect (
194
211
partial (self .m2m_changed , attr = field .name ),
195
212
sender = field .remote_field .through ,
@@ -200,13 +217,12 @@ def finalize(self, sender, **kwargs):
200
217
setattr (sender , self .manager_name , descriptor )
201
218
sender ._meta .simple_history_manager_attribute = self .manager_name
202
219
203
- for field in self . m2m_fields :
220
+ for field in m2m_fields :
204
221
m2m_model = self .create_history_m2m_model (
205
222
history_model , field .remote_field .through
206
223
)
207
224
self .m2m_models [field ] = m2m_model
208
225
209
- module = importlib .import_module (self .module )
210
226
setattr (module , m2m_model .__name__ , m2m_model )
211
227
212
228
m2m_descriptor = HistoryDescriptor (m2m_model )
@@ -235,46 +251,18 @@ def get_history_model_name(self, model):
235
251
)
236
252
237
253
def create_history_m2m_model (self , model , through_model ):
238
- attrs = {
239
- "__module__" : self .module ,
240
- "__str__" : lambda self : "{} as of {}" .format (
241
- self ._meta .verbose_name , self .history .history_date
242
- ),
243
- }
244
-
245
- app_module = "%s.models" % model ._meta .app_label
246
-
247
- if model .__module__ != self .module :
248
- # registered under different app
249
- attrs ["__module__" ] = self .module
250
- elif app_module != self .module :
251
- # Abuse an internal API because the app registry is loading.
252
- app = apps .app_configs [model ._meta .app_label ]
253
- models_module = app .name
254
- attrs ["__module__" ] = models_module
255
-
256
- # Get the primary key to the history model this model will look up to
257
- attrs ["m2m_history_id" ] = self ._get_history_id_field ()
258
- attrs ["history" ] = models .ForeignKey (
259
- model ,
260
- db_constraint = False ,
261
- on_delete = models .DO_NOTHING ,
262
- )
263
- attrs ["instance_type" ] = through_model
254
+ attrs = {}
264
255
265
256
fields = self .copy_fields (through_model )
266
257
attrs .update (fields )
258
+ attrs .update (self .get_extra_fields_m2m (model , through_model , fields ))
267
259
268
260
name = self .get_history_model_name (through_model )
269
261
registered_models [through_model ._meta .db_table ] = through_model
270
- meta_fields = {"verbose_name" : name }
271
262
272
- if self .app :
273
- meta_fields ["app_label" ] = self .app
274
-
275
- attrs .update (Meta = type (str ("Meta" ), (), meta_fields ))
263
+ attrs .update (Meta = type ("Meta" , (), self .get_meta_options_m2m (through_model )))
276
264
277
- m2m_history_model = type (str (name ), ( models . Model ,) , attrs )
265
+ m2m_history_model = type (str (name ), self . m2m_bases , attrs )
278
266
279
267
return m2m_history_model
280
268
@@ -285,7 +273,7 @@ def create_history_model(self, model, inherited):
285
273
attrs = {
286
274
"__module__" : self .module ,
287
275
"_history_excluded_fields" : self .excluded_fields ,
288
- "_history_m2m_fields" : self .m2m_fields ,
276
+ "_history_m2m_fields" : self .get_m2m_fields_from_model ( model ) ,
289
277
}
290
278
291
279
app_module = "%s.models" % model ._meta .app_label
@@ -412,7 +400,7 @@ def _get_history_change_reason_field(self):
412
400
413
401
def _get_history_id_field (self ):
414
402
if self .history_id_field :
415
- history_id_field = self .history_id_field
403
+ history_id_field = self .history_id_field . clone ()
416
404
history_id_field .primary_key = True
417
405
history_id_field .editable = False
418
406
elif getattr (settings , "SIMPLE_HISTORY_HISTORY_ID_USE_UUID" , False ):
@@ -465,6 +453,25 @@ def _get_history_related_field(self, model):
465
453
else :
466
454
return {}
467
455
456
+ def get_extra_fields_m2m (self , model , through_model , fields ):
457
+ """Return dict of extra fields added to the m2m historical record model"""
458
+
459
+ extra_fields = {
460
+ "__module__" : model .__module__ ,
461
+ "__str__" : lambda self : "{} as of {}" .format (
462
+ self ._meta .verbose_name , self .history .history_date
463
+ ),
464
+ "history" : models .ForeignKey (
465
+ model ,
466
+ db_constraint = False ,
467
+ on_delete = models .DO_NOTHING ,
468
+ ),
469
+ "instance_type" : through_model ,
470
+ "m2m_history_id" : self ._get_history_id_field (),
471
+ }
472
+
473
+ return extra_fields
474
+
468
475
def get_extra_fields (self , model , fields ):
469
476
"""Return dict of extra fields added to the historical record model"""
470
477
@@ -577,6 +584,20 @@ def _date_indexing(self):
577
584
)
578
585
return result
579
586
587
+ def get_meta_options_m2m (self , through_model ):
588
+ """
589
+ Returns a dictionary of fields that will be added to
590
+ the Meta inner class of the m2m historical record model.
591
+ """
592
+ name = self .get_history_model_name (through_model )
593
+
594
+ meta_fields = {"verbose_name" : name }
595
+
596
+ if self .app :
597
+ meta_fields ["app_label" ] = self .app
598
+
599
+ return meta_fields
600
+
580
601
def get_meta_options (self , model ):
581
602
"""
582
603
Returns a dictionary of fields that will be added to
@@ -637,7 +658,7 @@ def m2m_changed(self, instance, action, attr, pk_set, reverse, **_):
637
658
self .create_historical_record (instance , "~" )
638
659
639
660
def create_historical_record_m2ms (self , history_instance , instance ):
640
- for field in self . m2m_fields :
661
+ for field in history_instance . _history_m2m_fields :
641
662
m2m_history_model = self .m2m_models [field ]
642
663
original_instance = history_instance .instance
643
664
through_model = getattr (original_instance , field .name ).through
@@ -657,7 +678,21 @@ def create_historical_record_m2ms(self, history_instance, instance):
657
678
)
658
679
insert_rows .append (m2m_history_model (** insert_row ))
659
680
660
- m2m_history_model .objects .bulk_create (insert_rows )
681
+ pre_create_historical_m2m_records .send (
682
+ sender = m2m_history_model ,
683
+ rows = insert_rows ,
684
+ history_instance = history_instance ,
685
+ instance = instance ,
686
+ field = field ,
687
+ )
688
+ created_rows = m2m_history_model .objects .bulk_create (insert_rows )
689
+ post_create_historical_m2m_records .send (
690
+ sender = m2m_history_model ,
691
+ created_rows = created_rows ,
692
+ history_instance = history_instance ,
693
+ instance = instance ,
694
+ field = field ,
695
+ )
661
696
662
697
def create_historical_record (self , instance , history_type , using = None ):
663
698
using = using if self .use_base_model_db else None
@@ -721,6 +756,14 @@ def get_history_user(self, instance):
721
756
722
757
return self .get_user (instance = instance , request = request )
723
758
759
+ def get_m2m_fields_from_model (self , model ):
760
+ m2m_fields = set (self .m2m_fields )
761
+ try :
762
+ m2m_fields .update (getattr (model , self .m2m_fields_model_field_name ))
763
+ except AttributeError :
764
+ pass
765
+ return [getattr (model , field .name ).field for field in m2m_fields ]
766
+
724
767
725
768
def transform_field (field ):
726
769
"""Customize field appropriately for use in historical model"""
@@ -880,12 +923,20 @@ def diff_against(self, old_history, excluded_fields=None, included_fields=None):
880
923
if excluded_fields is None :
881
924
excluded_fields = set ()
882
925
926
+ included_m2m_fields = {field .name for field in old_history ._history_m2m_fields }
883
927
if included_fields is None :
884
928
included_fields = {
885
929
f .name for f in old_history .instance_type ._meta .fields if f .editable
886
930
}
931
+ else :
932
+ included_m2m_fields = included_m2m_fields .intersection (included_fields )
887
933
888
- fields = set (included_fields ).difference (excluded_fields )
934
+ fields = (
935
+ set (included_fields )
936
+ .difference (included_m2m_fields )
937
+ .difference (excluded_fields )
938
+ )
939
+ m2m_fields = set (included_m2m_fields ).difference (excluded_fields )
889
940
890
941
changes = []
891
942
changed_fields = []
@@ -902,11 +953,10 @@ def diff_against(self, old_history, excluded_fields=None, included_fields=None):
902
953
changed_fields .append (field )
903
954
904
955
# Separately compare m2m fields:
905
- for field in old_history . _history_m2m_fields :
956
+ for field in m2m_fields :
906
957
# First retrieve a single item to get the field names from:
907
958
reference_history_m2m_item = (
908
- getattr (old_history , field .name ).first ()
909
- or getattr (self , field .name ).first ()
959
+ getattr (old_history , field ).first () or getattr (self , field ).first ()
910
960
)
911
961
history_field_names = []
912
962
if reference_history_m2m_item :
@@ -920,15 +970,13 @@ def diff_against(self, old_history, excluded_fields=None, included_fields=None):
920
970
if f .editable and f .name not in ["id" , "m2m_history_id" , "history" ]
921
971
]
922
972
923
- old_rows = list (
924
- getattr (old_history , field .name ).values (* history_field_names )
925
- )
926
- new_rows = list (getattr (self , field .name ).values (* history_field_names ))
973
+ old_rows = list (getattr (old_history , field ).values (* history_field_names ))
974
+ new_rows = list (getattr (self , field ).values (* history_field_names ))
927
975
928
976
if old_rows != new_rows :
929
- change = ModelChange (field . name , old_rows , new_rows )
977
+ change = ModelChange (field , old_rows , new_rows )
930
978
changes .append (change )
931
- changed_fields .append (field . name )
979
+ changed_fields .append (field )
932
980
933
981
return ModelDelta (changes , changed_fields , old_history , self )
934
982
0 commit comments