Skip to content

Commit 71d71b4

Browse files
author
rodrigo.nogueira
committed
Add system-wide custom bases settings for HistoricalRecords
- Add SIMPLE_HISTORY_CUSTOM_BASES setting for default historical model bases - Add SIMPLE_HISTORY_CUSTOM_M2M_BASES setting for M2M historical model bases - Explicit bases/m2m_bases parameters override settings (backward compatible) - Add comprehensive documentation with examples - Add test models and test cases for both settings - All 342 tests passing Fixes #1574
1 parent afae094 commit 71d71b4

File tree

4 files changed

+132
-0
lines changed

4 files changed

+132
-0
lines changed

docs/historical_model.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,24 @@ To change the auto-generated HistoricalRecord models base class from
295295
history = HistoricalRecords(bases=[RoutableModel])
296296
297297
298+
You can also set a system-wide default for all HistoricalRecord base classes using
299+
the ``SIMPLE_HISTORY_CUSTOM_BASES`` setting in your ``settings.py`` file:
300+
301+
.. code-block:: python
302+
303+
SIMPLE_HISTORY_CUSTOM_BASES = [RoutableModel, AuditModel]
304+
305+
This setting will be used for all ``HistoricalRecords`` instances that don't explicitly
306+
specify a ``bases`` parameter. The explicit ``bases`` parameter always takes precedence
307+
over this setting, allowing you to override the global default on a per-model basis.
308+
309+
For many-to-many historical models, you can use:
310+
311+
.. code-block:: python
312+
313+
SIMPLE_HISTORY_CUSTOM_M2M_BASES = [M2MRoutableModel]
314+
315+
298316
Excluded Fields
299317
--------------------------------
300318

simple_history/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,26 @@ def __init__(
153153
if excluded_field_kwargs is None:
154154
excluded_field_kwargs = {}
155155
self.excluded_field_kwargs = excluded_field_kwargs
156+
157+
if bases == (models.Model,):
158+
custom_bases = getattr(settings, "SIMPLE_HISTORY_CUSTOM_BASES", None)
159+
if custom_bases is not None:
160+
bases = custom_bases
161+
156162
try:
157163
if isinstance(bases, str):
158164
raise TypeError
159165
self.bases = (HistoricalChanges,) + tuple(bases)
160166
except TypeError:
161167
raise TypeError("The `bases` option must be a list or a tuple.")
168+
169+
if m2m_bases == (models.Model,):
170+
custom_m2m_bases = getattr(
171+
settings, "SIMPLE_HISTORY_CUSTOM_M2M_BASES", None
172+
)
173+
if custom_m2m_bases is not None:
174+
m2m_bases = custom_m2m_bases
175+
162176
try:
163177
if isinstance(m2m_bases, str):
164178
raise TypeError

simple_history/tests/models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,43 @@ class ConcreteUtil(AbstractBase):
479479
register(ConcreteUtil, bases=[AbstractBase])
480480

481481

482+
# Test SIMPLE_HISTORY_CUSTOM_BASES setting
483+
setattr(settings, "SIMPLE_HISTORY_CUSTOM_BASES", [IPAddressHistoricalModel])
484+
485+
486+
class PollWithDefaultCustomBases(models.Model):
487+
question = models.CharField(max_length=200)
488+
history = HistoricalRecords()
489+
490+
491+
class PollWithExplicitBasesOverride(models.Model):
492+
question = models.CharField(max_length=200)
493+
history = HistoricalRecords(bases=[SessionsHistoricalModel])
494+
495+
496+
delattr(settings, "SIMPLE_HISTORY_CUSTOM_BASES")
497+
498+
499+
# Test SIMPLE_HISTORY_CUSTOM_M2M_BASES setting
500+
setattr(settings, "SIMPLE_HISTORY_CUSTOM_M2M_BASES", [IPAddressHistoricalModel])
501+
502+
503+
class PollWithDefaultCustomM2MBases(models.Model):
504+
question = models.CharField(max_length=200)
505+
places = models.ManyToManyField("Place")
506+
history = HistoricalRecords(m2m_fields=[places])
507+
508+
509+
class PollWithExplicitM2MBasesOverride(models.Model):
510+
question = models.CharField(max_length=200)
511+
places = models.ManyToManyField("Place")
512+
history = HistoricalRecords(m2m_fields=[places], m2m_bases=[SessionsHistoricalModel])
513+
514+
515+
delattr(settings, "SIMPLE_HISTORY_CUSTOM_M2M_BASES")
516+
517+
518+
482519
class MultiOneToOne(models.Model):
483520
fk = models.ForeignKey(SecondLevelInheritedModel, on_delete=models.CASCADE)
484521

simple_history/tests/tests/test_models.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@
9797
PollWithManyToMany,
9898
PollWithManyToManyCustomHistoryID,
9999
PollWithManyToManyWithIPAddress,
100+
PollWithDefaultCustomBases,
101+
PollWithExplicitBasesOverride,
102+
PollWithDefaultCustomM2MBases,
103+
PollWithExplicitM2MBasesOverride,
100104
PollWithNonEditableField,
101105
PollWithQuerySetCustomizations,
102106
PollWithSelfManyToMany,
@@ -1476,6 +1480,65 @@ def test_invalid_bases(self):
14761480
for bases in invalid_bases:
14771481
self.assertRaises(TypeError, HistoricalRecords, bases=bases)
14781482

1483+
def test_custom_bases_from_settings(self):
1484+
"""Test that SIMPLE_HISTORY_CUSTOM_BASES setting is correctly applied."""
1485+
# PollWithDefaultCustomBases was created with SIMPLE_HISTORY_CUSTOM_BASES=[IPAddressHistoricalModel]
1486+
history_model = PollWithDefaultCustomBases.history.model
1487+
# Check that IPAddressHistoricalModel is in the MRO
1488+
self.assertIn(
1489+
"IPAddressHistoricalModel",
1490+
[cls.__name__ for cls in history_model.__mro__],
1491+
)
1492+
# Verify the historical model has ip_address field from IPAddressHistoricalModel
1493+
self.assertTrue(hasattr(history_model, "ip_address"))
1494+
1495+
def test_custom_bases_explicit_overrides_setting(self):
1496+
"""Test that explicit bases parameter overrides SIMPLE_HISTORY_CUSTOM_BASES."""
1497+
# PollWithExplicitBasesOverride was created with explicit bases=[SessionsHistoricalModel]
1498+
# even though SIMPLE_HISTORY_CUSTOM_BASES was set to [IPAddressHistoricalModel]
1499+
history_model = PollWithExplicitBasesOverride.history.model
1500+
# Should have SessionsHistoricalModel, not IPAddressHistoricalModel
1501+
self.assertIn(
1502+
"SessionsHistoricalModel",
1503+
[cls.__name__ for cls in history_model.__mro__],
1504+
)
1505+
self.assertTrue(hasattr(history_model, "session"))
1506+
# Should NOT have ip_address from IPAddressHistoricalModel
1507+
self.assertFalse(hasattr(history_model, "ip_address"))
1508+
1509+
def test_custom_m2m_bases_from_settings(self):
1510+
"""Test that SIMPLE_HISTORY_CUSTOM_M2M_BASES setting is correctly applied."""
1511+
# PollWithDefaultCustomM2MBases was created with SIMPLE_HISTORY_CUSTOM_M2M_BASES=[IPAddressHistoricalModel]
1512+
# Get the M2M historical model by its generated name
1513+
from django.apps import apps
1514+
m2m_history_model = apps.get_model(
1515+
"tests", "HistoricalPollWithDefaultCustomM2MBases_places"
1516+
)
1517+
# Check that IPAddressHistoricalModel is in the MRO
1518+
self.assertIn(
1519+
"IPAddressHistoricalModel",
1520+
[cls.__name__ for cls in m2m_history_model.__mro__],
1521+
)
1522+
# Verify the M2M historical model has ip_address field
1523+
self.assertTrue(hasattr(m2m_history_model, "ip_address"))
1524+
1525+
def test_custom_m2m_bases_explicit_overrides_setting(self):
1526+
"""Test that explicit m2m_bases parameter overrides SIMPLE_HISTORY_CUSTOM_M2M_BASES."""
1527+
# PollWithExplicitM2MBasesOverride was created with explicit m2m_bases=[SessionsHistoricalModel]
1528+
# even though SIMPLE_HISTORY_CUSTOM_M2M_BASES was set to [IPAddressHistoricalModel]
1529+
from django.apps import apps
1530+
m2m_history_model = apps.get_model(
1531+
"tests", "HistoricalPollWithExplicitM2MBasesOverride_places"
1532+
)
1533+
# Should have SessionsHistoricalModel, not IPAddressHistoricalModel
1534+
self.assertIn(
1535+
"SessionsHistoricalModel",
1536+
[cls.__name__ for cls in m2m_history_model.__mro__],
1537+
)
1538+
self.assertTrue(hasattr(m2m_history_model, "session"))
1539+
# Should NOT have ip_address from IPAddressHistoricalModel
1540+
self.assertFalse(hasattr(m2m_history_model, "ip_address"))
1541+
14791542
def test_import_related(self):
14801543
field_object = HistoricalChoice._meta.get_field("poll")
14811544
related_model = field_object.remote_field.related_model

0 commit comments

Comments
 (0)