1
1
import unittest
2
+ from dataclasses import dataclass
2
3
from datetime import datetime
4
+ from typing import Optional , Type
3
5
from unittest import skipUnless
4
6
from unittest .mock import Mock , patch
5
7
6
8
import django
7
9
from django .contrib .auth import get_user_model
8
10
from django .db import IntegrityError , transaction
11
+ from django .db .models import Model
9
12
from django .test import TestCase , TransactionTestCase , override_settings
10
13
from django .utils import timezone
11
14
12
15
from simple_history .exceptions import AlternativeManagerError , NotHistoricalModelError
16
+ from simple_history .manager import HistoryManager
17
+ from simple_history .models import HistoricalChanges
13
18
from simple_history .utils import (
14
19
bulk_create_with_history ,
15
20
bulk_update_with_history ,
21
+ get_historical_records_of_instance ,
16
22
get_history_manager_for_model ,
17
23
get_history_model_for_model ,
18
24
get_m2m_field_name ,
19
25
get_m2m_reverse_field_name ,
26
+ get_pk_name ,
20
27
update_change_reason ,
21
28
)
22
29
30
+ from ..external import models as external
23
31
from ..models import (
32
+ AbstractBase ,
33
+ AbstractModelCallable1 ,
34
+ BaseModel ,
35
+ Book ,
24
36
BulkCreateManyToManyModel ,
37
+ Choice ,
38
+ ConcreteAttr ,
39
+ ConcreteExternal ,
40
+ ConcreteUtil ,
41
+ Contact ,
42
+ ContactRegister ,
43
+ CustomManagerNameModel ,
25
44
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 ,
26
58
Place ,
27
59
Poll ,
28
60
PollChildBookWithManyToMany ,
29
61
PollChildRestaurantWithManyToMany ,
62
+ PollInfo ,
63
+ PollParentWithManyToMany ,
30
64
PollWithAlternativeManager ,
65
+ PollWithCustomManager ,
66
+ PollWithExcludedFKField ,
31
67
PollWithExcludeFields ,
32
68
PollWithHistoricalSessionAttr ,
33
69
PollWithManyToMany ,
34
70
PollWithManyToManyCustomHistoryID ,
35
71
PollWithManyToManyWithIPAddress ,
72
+ PollWithQuerySetCustomizations ,
36
73
PollWithSelfManyToMany ,
37
74
PollWithSeveralManyToMany ,
38
75
PollWithUniqueQuestion ,
76
+ Profile ,
77
+ Restaurant ,
39
78
Street ,
79
+ TestHistoricParticipanToHistoricOrganization ,
80
+ TestParticipantToHistoricOrganization ,
81
+ TrackedAbstractBaseA ,
82
+ TrackedConcreteBase ,
83
+ TrackedWithAbstractBase ,
84
+ TrackedWithConcreteBase ,
85
+ Voter ,
40
86
)
41
87
42
88
User = get_user_model ()
@@ -53,6 +99,171 @@ def test_update_change_reason_with_excluded_fields(self):
53
99
self .assertEqual (most_recent .history_change_reason , "Test change reason." )
54
100
55
101
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
+
56
267
class GetM2MFieldNamesTestCase (unittest .TestCase ):
57
268
def test__get_m2m_field_name__returns_expected_value (self ):
58
269
def field_names (model ):
0 commit comments