Skip to content

Commit 9f17e23

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 9f17e23

File tree

7 files changed

+141
-1
lines changed

7 files changed

+141
-1
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ Authors
122122
- Renaud Perrin (`leminaw <https://github.com/leminaw>`_)
123123
- Roberto Aguilar
124124
- Rod Xavier Bondoc
125+
- Rodrigo Nogueira (`rodrigobnogueira <https://github.com/rodrigobnogueira>`_)
125126
- Ross Lote
126127
- Ross Mechanic (`rossmechanic <https://github.com/rossmechanic>`_)
127128
- Ross Rogers

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Changes
44
Unreleased
55
----------
66

7+
- Added ``SIMPLE_HISTORY_CUSTOM_BASES`` setting to specify default base classes
8+
for historical models (gh-1574)
9+
- Added ``SIMPLE_HISTORY_CUSTOM_M2M_BASES`` setting to specify default base classes
10+
for M2M historical models (gh-1574)
11+
712

813
3.11.0 (2025-12-09)
914
-------------------

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

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ classifiers = [
3333
"Programming Language :: Python :: 3.11",
3434
"Programming Language :: Python :: 3.12",
3535
"Programming Language :: Python :: 3.13",
36-
"Programming Language :: Python :: 3.14",
3736
]
3837
dynamic = [
3938
"readme",

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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,44 @@ 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(
513+
m2m_fields=[places], m2m_bases=[SessionsHistoricalModel]
514+
)
515+
516+
517+
delattr(settings, "SIMPLE_HISTORY_CUSTOM_M2M_BASES")
518+
519+
482520
class MultiOneToOne(models.Model):
483521
fk = models.ForeignKey(SecondLevelInheritedModel, on_delete=models.CASCADE)
484522

simple_history/tests/tests/test_models.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,13 @@
9090
PollChildRestaurantWithManyToMany,
9191
PollInfo,
9292
PollWithAlternativeManager,
93+
PollWithDefaultCustomBases,
94+
PollWithDefaultCustomM2MBases,
9395
PollWithExcludedFieldsWithDefaults,
9496
PollWithExcludedFKField,
9597
PollWithExcludeFields,
98+
PollWithExplicitBasesOverride,
99+
PollWithExplicitM2MBasesOverride,
96100
PollWithHistoricalIPAddress,
97101
PollWithManyToMany,
98102
PollWithManyToManyCustomHistoryID,
@@ -1476,6 +1480,67 @@ 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 uses SIMPLE_HISTORY_CUSTOM_BASES
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 setting."""
1497+
# PollWithExplicitBasesOverride uses explicit bases parameter
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 applied."""
1511+
# PollWithDefaultCustomM2MBases uses SIMPLE_HISTORY_CUSTOM_M2M_BASES
1512+
# Get the M2M historical model by its generated name
1513+
from django.apps import apps
1514+
1515+
m2m_history_model = apps.get_model(
1516+
"tests", "HistoricalPollWithDefaultCustomM2MBases_places"
1517+
)
1518+
# Check that IPAddressHistoricalModel is in the MRO
1519+
self.assertIn(
1520+
"IPAddressHistoricalModel",
1521+
[cls.__name__ for cls in m2m_history_model.__mro__],
1522+
)
1523+
# Verify the M2M historical model has ip_address field
1524+
self.assertTrue(hasattr(m2m_history_model, "ip_address"))
1525+
1526+
def test_custom_m2m_bases_explicit_overrides_setting(self):
1527+
"""Test that explicit m2m_bases parameter overrides setting."""
1528+
# PollWithExplicitM2MBasesOverride uses explicit m2m_bases
1529+
# even though SIMPLE_HISTORY_CUSTOM_M2M_BASES was set
1530+
from django.apps import apps
1531+
1532+
m2m_history_model = apps.get_model(
1533+
"tests", "HistoricalPollWithExplicitM2MBasesOverride_places"
1534+
)
1535+
# Should have SessionsHistoricalModel, not IPAddressHistoricalModel
1536+
self.assertIn(
1537+
"SessionsHistoricalModel",
1538+
[cls.__name__ for cls in m2m_history_model.__mro__],
1539+
)
1540+
self.assertTrue(hasattr(m2m_history_model, "session"))
1541+
# Should NOT have ip_address from IPAddressHistoricalModel
1542+
self.assertFalse(hasattr(m2m_history_model, "ip_address"))
1543+
14791544
def test_import_related(self):
14801545
field_object = HistoricalChoice._meta.get_field("poll")
14811546
related_model = field_object.remote_field.related_model

0 commit comments

Comments
 (0)