Skip to content

Commit cc3a2c1

Browse files
tbeadleRoss Mechanic
authored andcommitted
Ensure custom arguments for fields are included in historical models' fields. (#462)
Previously, if a custom field class was used for a foreign key and that class accepted custom kwargs, those kwargs would not be included in the creation of the historical model's field and would break the migration. To address this, we'll use the deconstruct() method of the field to get the arguments that should be passed to it and override the ones we need to for the historical model's field.
1 parent 3183611 commit cc3a2c1

File tree

6 files changed

+144
-32
lines changed

6 files changed

+144
-32
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Unreleased
77
- Fix header on history pages when custom site_header is used (gh-448)
88
- Modify `pre_create_historircal_record` to pass `history_instance` for ease of customization (gh-421)
99
- Raise warning if HistoricalRecords(inherit=False) is in an abstract model (gh-341)
10+
- Ensure custom arguments for fields are included in historical models' fields (gh-431)
1011

1112
2.5.1 (2018-10-19)
1213
------------------

simple_history/models.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -184,29 +184,22 @@ def copy_fields(self, model):
184184
field.__class__ = models.IntegerField
185185
if isinstance(field, models.ForeignKey):
186186
old_field = field
187-
field_arguments = {'db_constraint': False}
187+
old_swappable = old_field.swappable
188+
old_field.swappable = False
189+
try:
190+
_name, _path, args, field_args = old_field.deconstruct()
191+
finally:
192+
old_field.swappable = old_swappable
188193
if getattr(old_field, 'one_to_one', False) \
189194
or isinstance(old_field, models.OneToOneField):
190195
FieldType = models.ForeignKey
191196
else:
192197
FieldType = type(old_field)
193-
if getattr(old_field, 'to_fields', []):
194-
field_arguments['to_field'] = old_field.to_fields[0]
195-
if getattr(old_field, 'db_column', None):
196-
field_arguments['db_column'] = old_field.db_column
197-
198-
# If old_field.remote_field.model is 'self' then we have a
199-
# case where object has a foreign key to itself. In this case
200-
# we need to set the `model` value of the field to a model. We
201-
# can use the old_field.model value.
202-
if isinstance(old_field.remote_field.model, str) and \
203-
old_field.remote_field.model == 'self':
204-
object_to = old_field.model
205-
else:
206-
object_to = old_field.remote_field.model
207198

208-
field = FieldType(
209-
object_to,
199+
# Override certain arguments passed when creating the field
200+
# so that they work for the historical field.
201+
field_args.update(
202+
db_constraint=False,
210203
related_name='+',
211204
null=True,
212205
blank=True,
@@ -215,7 +208,10 @@ def copy_fields(self, model):
215208
serialize=True,
216209
unique=False,
217210
on_delete=models.DO_NOTHING,
218-
**field_arguments
211+
)
212+
field = FieldType(
213+
*args,
214+
**field_args
219215
)
220216
field.name = old_field.name
221217
else:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Generated by Django 2.1 on 2018-10-19 21:53
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import simple_history.models
7+
from .. import models as my_models
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
('migration_test_app', '0001_initial'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='HistoricalModelWithCustomAttrForeignKey',
19+
fields=[
20+
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
21+
('history_id', models.AutoField(primary_key=True, serialize=False)),
22+
('history_change_reason', models.CharField(max_length=100, null=True)),
23+
('history_date', models.DateTimeField()),
24+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
25+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
26+
('what_i_mean', my_models.CustomAttrNameForeignKey(attr_name='custom_attr_name', blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='migration_test_app.WhatIMean')),
27+
],
28+
options={
29+
'verbose_name': 'historical model with custom attr foreign key',
30+
'ordering': ('-history_date', '-history_id'),
31+
'get_latest_by': 'history_date',
32+
},
33+
bases=(simple_history.models.HistoricalChanges, models.Model),
34+
),
35+
migrations.CreateModel(
36+
name='ModelWithCustomAttrForeignKey',
37+
fields=[
38+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39+
('what_i_mean', my_models.CustomAttrNameForeignKey(attr_name='custom_attr_name', on_delete=django.db.models.deletion.CASCADE, to='migration_test_app.WhatIMean')),
40+
],
41+
),
42+
]

simple_history/registry_tests/migration_test_app/models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,31 @@ class WhatIMean(DoYouKnow):
1313
class Yar(models.Model):
1414
what = models.ForeignKey(WhatIMean, on_delete=models.CASCADE)
1515
history = HistoricalRecords()
16+
17+
18+
class CustomAttrNameForeignKey(models.ForeignKey):
19+
def __init__(self, *args, **kwargs):
20+
self.attr_name = kwargs.pop("attr_name", None)
21+
super(CustomAttrNameForeignKey, self).__init__(*args, **kwargs)
22+
23+
def get_attname(self):
24+
return self.attr_name or super(
25+
CustomAttrNameForeignKey, self
26+
).get_attname()
27+
28+
def deconstruct(self):
29+
name, path, args, kwargs = super(
30+
CustomAttrNameForeignKey, self
31+
).deconstruct()
32+
if self.attr_name:
33+
kwargs["attr_name"] = self.attr_name
34+
return name, path, args, kwargs
35+
36+
37+
class ModelWithCustomAttrForeignKey(models.Model):
38+
what_i_mean = CustomAttrNameForeignKey(
39+
WhatIMean,
40+
models.CASCADE,
41+
attr_name='custom_attr_name',
42+
)
43+
history = HistoricalRecords()

simple_history/registry_tests/tests.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
Restaurant, TrackedAbstractBaseA,
1717
TrackedAbstractBaseB, TrackedWithAbstractBase,
1818
TrackedWithConcreteBase, UserAccessorDefault,
19-
UserAccessorOverride, UUIDRegisterModel, Voter)
19+
UserAccessorOverride, UUIDRegisterModel, Voter,
20+
ModelWithCustomAttrForeignKey)
2021

2122
get_model = apps.get_model
2223
User = get_user_model()
@@ -109,15 +110,15 @@ def test_tracked_abstract_base(self):
109110

110111
def test_tracked_concrete_base(self):
111112
self.assertEqual(
112-
[
113+
sorted(
113114
f.attname
114115
for f in TrackedWithConcreteBase.history.model._meta.fields
115-
],
116-
[
116+
),
117+
sorted([
117118
'id', 'trackedconcretebase_ptr_id', 'history_id',
118119
'history_change_reason', 'history_date', 'history_user_id',
119120
'history_type',
120-
],
121+
]),
121122
)
122123

123124
def test_multiple_tracked_bases(self):
@@ -128,39 +129,55 @@ class TrackedWithMultipleAbstractBases(
128129

129130
def test_tracked_abstract_and_untracked_concrete_base(self):
130131
self.assertEqual(
131-
[f.attname for f in InheritTracking1.history.model._meta.fields],
132-
[
132+
sorted(
133+
f.attname for f in InheritTracking1.history.model._meta.fields
134+
),
135+
sorted([
133136
'id', 'untrackedconcretebase_ptr_id', 'history_id',
134137
'history_change_reason', 'history_date',
135138
'history_user_id', 'history_type',
136-
],
139+
]),
137140
)
138141

139142
def test_indirect_tracked_abstract_base(self):
140143
self.assertEqual(
141-
[f.attname for f in InheritTracking2.history.model._meta.fields],
142-
[
144+
sorted(
145+
f.attname for f in InheritTracking2.history.model._meta.fields
146+
),
147+
sorted([
143148
'id', 'baseinherittracking2_ptr_id', 'history_id',
144149
'history_change_reason', 'history_date',
145150
'history_user_id', 'history_type',
146-
],
151+
]),
147152
)
148153

149154
def test_indirect_tracked_concrete_base(self):
150155
self.assertEqual(
151-
[f.attname for f in InheritTracking3.history.model._meta.fields],
152-
[
156+
sorted(
157+
f.attname for f in InheritTracking3.history.model._meta.fields
158+
),
159+
sorted([
153160
'id', 'baseinherittracking3_ptr_id', 'history_id',
154161
'history_change_reason', 'history_date',
155162
'history_user_id', 'history_type',
156-
],
163+
]),
157164
)
158165

159166
def test_registering_with_tracked_abstract_base(self):
160167
with self.assertRaises(exceptions.MultipleRegistrationsError):
161168
register(InheritTracking4)
162169

163170

171+
class TestCustomAttrForeignKey(TestCase):
172+
""" https://github.com/treyhunner/django-simple-history/issues/431 """
173+
174+
def test_custom_attr(self):
175+
field = ModelWithCustomAttrForeignKey.history.model._meta.get_field(
176+
'poll',
177+
)
178+
self.assertEqual(field.attr_name, 'custom_poll')
179+
180+
164181
class TestMigrate(TestCase):
165182

166183
def test_makemigration_command(self):

simple_history/tests/models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,34 @@ def get_absolute_url(self):
5959
return reverse('poll-detail', kwargs={'pk': self.pk})
6060

6161

62+
class CustomAttrNameForeignKey(models.ForeignKey):
63+
def __init__(self, *args, **kwargs):
64+
self.attr_name = kwargs.pop("attr_name", None)
65+
super(CustomAttrNameForeignKey, self).__init__(*args, **kwargs)
66+
67+
def get_attname(self):
68+
return self.attr_name or super(
69+
CustomAttrNameForeignKey, self
70+
).get_attname()
71+
72+
def deconstruct(self):
73+
name, path, args, kwargs = super(
74+
CustomAttrNameForeignKey, self
75+
).deconstruct()
76+
if self.attr_name:
77+
kwargs["attr_name"] = self.attr_name
78+
return name, path, args, kwargs
79+
80+
81+
class ModelWithCustomAttrForeignKey(models.Model):
82+
poll = CustomAttrNameForeignKey(
83+
Poll,
84+
models.CASCADE,
85+
attr_name='custom_poll',
86+
)
87+
history = HistoricalRecords()
88+
89+
6290
class Temperature(models.Model):
6391
location = models.CharField(max_length=200)
6492
temperature = models.IntegerField()

0 commit comments

Comments
 (0)