1515from django .db import models
1616from django .db .models import ManyToManyField
1717from django .db .models .fields .proxy import OrderWrt
18- from django .db .models .fields .related import ForeignKey
18+ from django .db .models .fields .related import ForeignKey , lazy_related_operation
1919from django .db .models .fields .related_descriptors import (
2020 ForwardManyToOneDescriptor ,
2121 ReverseManyToOneDescriptor ,
@@ -84,6 +84,8 @@ class HistoricalRecords:
8484 DEFAULT_MODEL_NAME_PREFIX = "Historical"
8585
8686 thread = context = LocalContext () # retain thread for backwards compatibility
87+ # Key is the m2m field and value is a tuple where first entry is the historical m2m
88+ # model and second is the through model
8789 m2m_models = {}
8890
8991 def __init__ (
@@ -222,13 +224,6 @@ def finalize(self, sender, **kwargs):
222224
223225 m2m_fields = self .get_m2m_fields_from_model (sender )
224226
225- for field in m2m_fields :
226- m2m_changed .connect (
227- partial (self .m2m_changed , attr = field .name ),
228- sender = field .remote_field .through ,
229- weak = False ,
230- )
231-
232227 descriptor = HistoryDescriptor (
233228 history_model ,
234229 manager = self .history_manager ,
@@ -238,15 +233,29 @@ def finalize(self, sender, **kwargs):
238233 sender ._meta .simple_history_manager_attribute = self .manager_name
239234
240235 for field in m2m_fields :
241- m2m_model = self .create_history_m2m_model (
242- history_model , field .remote_field .through
243- )
244- self .m2m_models [field ] = m2m_model
245236
246- setattr (module , m2m_model .__name__ , m2m_model )
237+ def resolve_through_model (history_model , through_model ):
238+ m2m_changed .connect (
239+ partial (self .m2m_changed , attr = field .name ),
240+ sender = through_model ,
241+ weak = False ,
242+ )
243+ m2m_model = self .create_history_m2m_model (history_model , through_model )
244+ # Save the created history model and the resolved through model together
245+ # for reference later
246+ self .m2m_models [field ] = (m2m_model , through_model )
247+
248+ setattr (module , m2m_model .__name__ , m2m_model )
247249
248- m2m_descriptor = HistoryDescriptor (m2m_model )
249- setattr (history_model , field .name , m2m_descriptor )
250+ m2m_descriptor = HistoryDescriptor (m2m_model )
251+ setattr (history_model , field .name , m2m_descriptor )
252+
253+ # Lazily generate the historical m2m models for the fields when all of the
254+ # associated models have been fully loaded. This handles resolving through
255+ # models referenced as strings. This is how django m2m fields handle this.
256+ lazy_related_operation (
257+ resolve_through_model , history_model , field .remote_field .through
258+ )
250259
251260 def get_history_model_name (self , model ):
252261 if not self .custom_model_name :
@@ -685,9 +694,7 @@ def m2m_changed(self, instance, action, attr, pk_set, reverse, **_):
685694
686695 def create_historical_record_m2ms (self , history_instance , instance ):
687696 for field in history_instance ._history_m2m_fields :
688- m2m_history_model = self .m2m_models [field ]
689- original_instance = history_instance .instance
690- through_model = getattr (original_instance , field .name ).through
697+ m2m_history_model , through_model = self .m2m_models [field ]
691698 through_model_field_names = [f .name for f in through_model ._meta .fields ]
692699 through_model_fk_field_names = [
693700 f .name for f in through_model ._meta .fields if isinstance (f , ForeignKey )
0 commit comments