Skip to content

Commit 23c37dd

Browse files
committed
Code cleanup of some util functions
Incl. removing `HistoryManager.get_super_queryset()` and renaming the following `utils` functions: * get_history_manager_from_history -> get_historical_records_of_instance * get_app_model_primary_key_name -> get_pk_name Also added some tests for some of the changed util functions.
1 parent c2576e3 commit 23c37dd

File tree

6 files changed

+266
-40
lines changed

6 files changed

+266
-40
lines changed

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ Unreleased
1111
- Added ``delete_without_historical_record()`` to all history-tracked model objects,
1212
which complements ``save_without_historical_record()`` (gh-1387)
1313

14+
**Breaking changes:**
15+
16+
- Removed ``HistoryManager.get_super_queryset()`` (gh-1387)
17+
- Renamed the ``utils`` functions ``get_history_manager_from_history()``
18+
to ``get_historical_records_of_instance()`` and ``get_app_model_primary_key_name()``
19+
to ``get_pk_name()`` (gh-1387)
20+
1421
**Deprecations:**
1522

1623
- Deprecated the undocumented ``HistoricalRecords.thread`` - use

simple_history/manager.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
from django.db.models import Exists, OuterRef, Q, QuerySet
44
from django.utils import timezone
55

6-
from simple_history.utils import (
7-
get_app_model_primary_key_name,
8-
get_change_reason_from_object,
9-
)
6+
from . import utils
107

118
# when converting a historical record to an instance, this attribute is added
129
# to the instance so that code can reverse the instance to its historical record
@@ -118,16 +115,13 @@ def __init__(self, model, instance=None):
118115
self.model = model
119116
self.instance = instance
120117

121-
def get_super_queryset(self):
122-
return super().get_queryset()
123-
124118
def get_queryset(self):
125-
qs = self.get_super_queryset()
119+
qs = super().get_queryset()
126120
if self.instance is None:
127121
return qs
128122

129-
key_name = get_app_model_primary_key_name(self.instance)
130-
return self.get_super_queryset().filter(**{key_name: self.instance.pk})
123+
pk_name = utils.get_pk_name(self.instance._meta.model)
124+
return qs.filter(**{pk_name: self.instance.pk})
131125

132126
def most_recent(self):
133127
"""
@@ -241,7 +235,7 @@ def bulk_history_create(
241235
instance, "_history_date", default_date or timezone.now()
242236
),
243237
history_user=history_user,
244-
history_change_reason=get_change_reason_from_object(instance)
238+
history_change_reason=utils.get_change_reason_from_object(instance)
245239
or default_change_reason,
246240
history_type=history_type,
247241
**{

simple_history/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ def get_next_record(self):
567567
"""
568568
Get the next history record for the instance. `None` if last.
569569
"""
570-
history = utils.get_history_manager_from_history(self)
570+
history = utils.get_historical_records_of_instance(self)
571571
return (
572572
history.filter(history_date__gt=self.history_date)
573573
.order_by("history_date")
@@ -578,7 +578,7 @@ def get_prev_record(self):
578578
"""
579579
Get the previous history record for the instance. `None` if first.
580580
"""
581-
history = utils.get_history_manager_from_history(self)
581+
history = utils.get_historical_records_of_instance(self)
582582
return (
583583
history.filter(history_date__lt=self.history_date)
584584
.order_by("history_date")

simple_history/tests/tests/test_utils.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,88 @@
11
import unittest
2+
from dataclasses import dataclass
23
from datetime import datetime
4+
from typing import Optional, Type
35
from unittest import skipUnless
46
from unittest.mock import Mock, patch
57

68
import django
79
from django.contrib.auth import get_user_model
810
from django.db import IntegrityError, transaction
11+
from django.db.models import Model
912
from django.test import TestCase, TransactionTestCase, override_settings
1013
from django.utils import timezone
1114

1215
from simple_history.exceptions import AlternativeManagerError, NotHistoricalModelError
16+
from simple_history.manager import HistoryManager
17+
from simple_history.models import HistoricalChanges
1318
from simple_history.utils import (
1419
bulk_create_with_history,
1520
bulk_update_with_history,
21+
get_historical_records_of_instance,
1622
get_history_manager_for_model,
1723
get_history_model_for_model,
1824
get_m2m_field_name,
1925
get_m2m_reverse_field_name,
26+
get_pk_name,
2027
update_change_reason,
2128
)
2229

30+
from ..external import models as external
2331
from ..models import (
32+
AbstractBase,
33+
AbstractModelCallable1,
34+
BaseModel,
35+
Book,
2436
BulkCreateManyToManyModel,
37+
Choice,
38+
ConcreteAttr,
39+
ConcreteExternal,
40+
ConcreteUtil,
41+
Contact,
42+
ContactRegister,
43+
CustomManagerNameModel,
2544
Document,
45+
ExternalModelSpecifiedWithAppParam,
46+
ExternalModelWithAppLabel,
47+
FirstLevelInheritedModel,
48+
HardbackBook,
49+
HistoricalBook,
50+
HistoricalPoll,
51+
HistoricalPollInfo,
52+
InheritTracking1,
53+
ModelWithHistoryInDifferentApp,
54+
ModelWithHistoryUsingBaseModelDb,
55+
OverrideModelNameAsCallable,
56+
OverrideModelNameRegisterMethod1,
57+
OverrideModelNameUsingBaseModel1,
2658
Place,
2759
Poll,
2860
PollChildBookWithManyToMany,
2961
PollChildRestaurantWithManyToMany,
62+
PollInfo,
63+
PollParentWithManyToMany,
3064
PollWithAlternativeManager,
65+
PollWithCustomManager,
66+
PollWithExcludedFKField,
3167
PollWithExcludeFields,
3268
PollWithHistoricalSessionAttr,
3369
PollWithManyToMany,
3470
PollWithManyToManyCustomHistoryID,
3571
PollWithManyToManyWithIPAddress,
72+
PollWithQuerySetCustomizations,
3673
PollWithSelfManyToMany,
3774
PollWithSeveralManyToMany,
3875
PollWithUniqueQuestion,
76+
Profile,
77+
Restaurant,
3978
Street,
79+
TestHistoricParticipanToHistoricOrganization,
80+
TestParticipantToHistoricOrganization,
81+
TrackedAbstractBaseA,
82+
TrackedConcreteBase,
83+
TrackedWithAbstractBase,
84+
TrackedWithConcreteBase,
85+
Voter,
4086
)
4187

4288
User = get_user_model()
@@ -53,6 +99,171 @@ def test_update_change_reason_with_excluded_fields(self):
5399
self.assertEqual(most_recent.history_change_reason, "Test change reason.")
54100

55101

102+
@dataclass
103+
class HistoryTrackedModelTestInfo:
104+
model: Type[Model]
105+
history_manager_name: Optional[str]
106+
107+
def __init__(
108+
self,
109+
model: Type[Model],
110+
history_manager_name: Optional[str] = "history",
111+
):
112+
self.model = model
113+
self.history_manager_name = history_manager_name
114+
115+
116+
class GetHistoryManagerAndModelHelpersTestCase(TestCase):
117+
@classmethod
118+
def setUpClass(cls):
119+
super().setUpClass()
120+
121+
H = HistoryTrackedModelTestInfo
122+
cls.history_tracked_models = [
123+
H(Choice),
124+
H(ConcreteAttr),
125+
H(ConcreteExternal),
126+
H(ConcreteUtil),
127+
H(Contact),
128+
H(ContactRegister),
129+
H(CustomManagerNameModel, "log"),
130+
H(ExternalModelSpecifiedWithAppParam, "histories"),
131+
H(ExternalModelWithAppLabel),
132+
H(InheritTracking1),
133+
H(ModelWithHistoryInDifferentApp),
134+
H(ModelWithHistoryUsingBaseModelDb),
135+
H(OverrideModelNameAsCallable),
136+
H(OverrideModelNameRegisterMethod1),
137+
H(OverrideModelNameUsingBaseModel1),
138+
H(Poll),
139+
H(PollChildBookWithManyToMany),
140+
H(PollWithAlternativeManager),
141+
H(PollWithCustomManager),
142+
H(PollWithExcludedFKField),
143+
H(PollWithHistoricalSessionAttr),
144+
H(PollWithManyToMany),
145+
H(PollWithManyToManyCustomHistoryID),
146+
H(PollWithManyToManyWithIPAddress),
147+
H(PollWithQuerySetCustomizations),
148+
H(PollWithSelfManyToMany),
149+
H(Restaurant, "updates"),
150+
H(TestHistoricParticipanToHistoricOrganization),
151+
H(TrackedConcreteBase),
152+
H(TrackedWithAbstractBase),
153+
H(TrackedWithConcreteBase),
154+
H(Voter),
155+
H(external.ExternalModel),
156+
H(external.ExternalModelRegistered, "histories"),
157+
H(external.Poll),
158+
]
159+
cls.models_without_history_manager = [
160+
H(AbstractBase, None),
161+
H(AbstractModelCallable1, None),
162+
H(BaseModel, None),
163+
H(FirstLevelInheritedModel, None),
164+
H(HardbackBook, None),
165+
H(Place, None),
166+
H(PollParentWithManyToMany, None),
167+
H(Profile, None),
168+
H(TestParticipantToHistoricOrganization, None),
169+
H(TrackedAbstractBaseA, None),
170+
]
171+
172+
def test__get_history_manager_for_model(self):
173+
"""Test that ``get_history_manager_for_model()`` returns the expected value
174+
for various models."""
175+
176+
def assert_history_manager(history_manager, info: HistoryTrackedModelTestInfo):
177+
expected_manager = getattr(info.model, info.history_manager_name)
178+
expected_historical_model = expected_manager.model
179+
historical_model = history_manager.model
180+
# Can't compare the managers directly, as the history manager classes are
181+
# dynamically created through `HistoryDescriptor`
182+
self.assertIsInstance(history_manager, HistoryManager)
183+
self.assertIsInstance(expected_manager, HistoryManager)
184+
self.assertTrue(issubclass(historical_model, HistoricalChanges))
185+
self.assertEqual(historical_model.instance_type, info.model)
186+
self.assertEqual(historical_model, expected_historical_model)
187+
188+
for model_info in self.history_tracked_models:
189+
with self.subTest(model_info=model_info):
190+
model = model_info.model
191+
manager = get_history_manager_for_model(model)
192+
assert_history_manager(manager, model_info)
193+
194+
for model_info in self.models_without_history_manager:
195+
with self.subTest(model_info=model_info):
196+
model = model_info.model
197+
with self.assertRaises(NotHistoricalModelError):
198+
get_history_manager_for_model(model)
199+
200+
def test__get_history_model_for_model(self):
201+
"""Test that ``get_history_model_for_model()`` returns the expected value
202+
for various models."""
203+
for model_info in self.history_tracked_models:
204+
with self.subTest(model_info=model_info):
205+
model = model_info.model
206+
historical_model = get_history_model_for_model(model)
207+
self.assertTrue(issubclass(historical_model, HistoricalChanges))
208+
self.assertEqual(historical_model.instance_type, model)
209+
210+
for model_info in self.models_without_history_manager:
211+
with self.subTest(model_info=model_info):
212+
model = model_info.model
213+
with self.assertRaises(NotHistoricalModelError):
214+
get_history_model_for_model(model)
215+
216+
def test__get_pk_name(self):
217+
"""Test that ``get_pk_name()`` returns the expected value for various models."""
218+
self.assertEqual(get_pk_name(Poll), "id")
219+
self.assertEqual(get_pk_name(PollInfo), "poll_id")
220+
self.assertEqual(get_pk_name(Book), "isbn")
221+
222+
self.assertEqual(get_pk_name(HistoricalPoll), "history_id")
223+
self.assertEqual(get_pk_name(HistoricalPollInfo), "history_id")
224+
self.assertEqual(get_pk_name(HistoricalBook), "history_id")
225+
226+
227+
class GetHistoricalRecordsOfInstanceTestCase(TestCase):
228+
def test__get_historical_records_of_instance(self):
229+
"""Test that ``get_historical_records_of_instance()`` returns the expected
230+
queryset for history-tracked model instances."""
231+
poll1 = Poll.objects.create(pub_date=timezone.now())
232+
poll1_history = poll1.history.all()
233+
(record1_1,) = poll1_history
234+
self.assertQuerySetEqual(
235+
get_historical_records_of_instance(record1_1),
236+
poll1_history,
237+
)
238+
239+
poll2 = Poll.objects.create(pub_date=timezone.now())
240+
poll2.question = "?"
241+
poll2.save()
242+
poll2_history = poll2.history.all()
243+
(record2_2, record2_1) = poll2_history
244+
self.assertQuerySetEqual(
245+
get_historical_records_of_instance(record2_1),
246+
poll2_history,
247+
)
248+
self.assertQuerySetEqual(
249+
get_historical_records_of_instance(record2_2),
250+
poll2_history,
251+
)
252+
253+
poll3 = Poll.objects.create(id=123, pub_date=timezone.now())
254+
poll3.delete()
255+
poll3_history = Poll.history.filter(id=123)
256+
(record3_2, record3_1) = poll3_history
257+
self.assertQuerySetEqual(
258+
get_historical_records_of_instance(record3_1),
259+
poll3_history,
260+
)
261+
self.assertQuerySetEqual(
262+
get_historical_records_of_instance(record3_2),
263+
poll3_history,
264+
)
265+
266+
56267
class GetM2MFieldNamesTestCase(unittest.TestCase):
57268
def test__get_m2m_field_name__returns_expected_value(self):
58269
def field_names(model):

simple_history/tests/tests/utils.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ def assertRecordValues(self, record, klass: Type[Model], values_dict: dict):
2626
:param klass: The type of the history-tracked class of ``record``.
2727
:param values_dict: Field names of ``record`` mapped to their expected values.
2828
"""
29-
for key, value in values_dict.items():
30-
self.assertEqual(getattr(record, key), value)
31-
32-
self.assertEqual(record.history_object.__class__, klass)
33-
for key, value in values_dict.items():
34-
if key not in ("history_type", "history_change_reason"):
35-
self.assertEqual(getattr(record.history_object, key), value)
29+
for field_name, expected_value in values_dict.items():
30+
self.assertEqual(getattr(record, field_name), expected_value)
31+
32+
history_object = record.history_object
33+
self.assertEqual(history_object.__class__, klass)
34+
for field_name, expected_value in values_dict.items():
35+
if field_name not in ("history_type", "history_change_reason"):
36+
self.assertEqual(getattr(history_object, field_name), expected_value)
3637

3738

3839
class TestDbRouter:

0 commit comments

Comments
 (0)