From 588bab2133bab34bd3a5034872b16b204209b819 Mon Sep 17 00:00:00 2001 From: Trent Date: Thu, 12 Sep 2024 14:48:54 -0400 Subject: [PATCH] #1266 resolving through tables defined as strings on m2m relations --- simple_history/models.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/simple_history/models.py b/simple_history/models.py index 3ffe42a5..85d4f3bf 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -15,7 +15,7 @@ from django.db import models from django.db.models import ManyToManyField from django.db.models.fields.proxy import OrderWrt -from django.db.models.fields.related import ForeignKey +from django.db.models.fields.related import ForeignKey, lazy_related_operation from django.db.models.fields.related_descriptors import ( ForwardManyToOneDescriptor, ReverseManyToOneDescriptor, @@ -84,6 +84,8 @@ class HistoricalRecords: DEFAULT_MODEL_NAME_PREFIX = "Historical" thread = context = LocalContext() # retain thread for backwards compatibility + # Key is the m2m field and value is a tuple where first entry is the historical m2m + # model and second is the through model m2m_models = {} def __init__( @@ -238,15 +240,24 @@ def finalize(self, sender, **kwargs): sender._meta.simple_history_manager_attribute = self.manager_name for field in m2m_fields: - m2m_model = self.create_history_m2m_model( - history_model, field.remote_field.through - ) - self.m2m_models[field] = m2m_model - setattr(module, m2m_model.__name__, m2m_model) + def resolve_through_model(history_model, through_model): + m2m_model = self.create_history_m2m_model(history_model, through_model) + # Save the created history model and the resolved through model together + # for reference later + self.m2m_models[field] = (m2m_model, through_model) + + setattr(module, m2m_model.__name__, m2m_model) - m2m_descriptor = HistoryDescriptor(m2m_model) - setattr(history_model, field.name, m2m_descriptor) + m2m_descriptor = HistoryDescriptor(m2m_model) + setattr(history_model, field.name, m2m_descriptor) + + # Lazily generate the historical m2m models for the fields when all of the + # associated models have been fully loaded. This handles resolving through + # models referenced as strings. This is how django m2m fields handle this. + lazy_related_operation( + resolve_through_model, history_model, field.remote_field.through + ) def get_history_model_name(self, model): if not self.custom_model_name: @@ -685,9 +696,7 @@ def m2m_changed(self, instance, action, attr, pk_set, reverse, **_): def create_historical_record_m2ms(self, history_instance, instance): for field in history_instance._history_m2m_fields: - m2m_history_model = self.m2m_models[field] - original_instance = history_instance.instance - through_model = getattr(original_instance, field.name).through + m2m_history_model, through_model = self.m2m_models[field] through_model_field_names = [f.name for f in through_model._meta.fields] through_model_fk_field_names = [ f.name for f in through_model._meta.fields if isinstance(f, ForeignKey)