11import unittest
2+ from dataclasses import dataclass
23from datetime import datetime
4+ from typing import Optional , Type
35from unittest import skipUnless
46from unittest .mock import Mock , patch
57
68import django
79from django .contrib .auth import get_user_model
810from django .db import IntegrityError , transaction
11+ from django .db .models import Model
912from django .test import TestCase , TransactionTestCase , override_settings
1013from django .utils import timezone
1114
1215from simple_history .exceptions import AlternativeManagerError , NotHistoricalModelError
16+ from simple_history .manager import HistoryManager
17+ from simple_history .models import HistoricalChanges
1318from 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
2331from ..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
4288User = 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+
56267class GetM2MFieldNamesTestCase (unittest .TestCase ):
57268 def test__get_m2m_field_name__returns_expected_value (self ):
58269 def field_names (model ):
0 commit comments