Repository: jazzband/django-simple-history

Issue:
django-simple-history does not respect 'deconstruct' kwargs for custom foreignkey fields
**Describe the bug**
After subclassing ForeignKey to override get_attname, and defining a deconstruct method to carry the additional parameter, makemigrations creates the correct migrations but django-simple-history ignores the extra parameters.  The resulting models are unusable.  A custom migration fixes the issue but will be replaced on the next makemigrations operation. 

The custom foreign key field is motivated and inspired by:
https://stackoverflow.com/questions/21524830/modifying-djangos-default-id-suffix-for-related-objects/24427909#24427909

**To Reproduce**
Steps to reproduce the behavior:

Deconstruct, as defined in Django docs [here](https://docs.djangoproject.com/en/2.1/howto/custom-model-fields/#field-deconstruction), is used to carry the attr_name argument into the migrations.  Here is the class: 
```
class CustomAttrNameForeignKey(models.ForeignKey):
    def __init__(self, *args, **kwargs):
        self.attr_name = kwargs.pop('attr_name', None)
        super().__init__(*args, **kwargs)

    def get_attname(self):
        return self.attr_name or super().get_attname()

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if self.attr_name:
            kwargs['attr_name'] = self.attr_name
        return name, path, args, kwargs
```

**Expected behavior**
When running make migration, Django creates 2 migrations as expected.  One for the model and one for the historical model.  However, the migration associated with the historical model does not have the needed 'attr_name' argument.  This makes all operations (save, get_or_create, etc.) on model instances fail.

Here is an example of the generated migration code:
```
        migrations.AlterField(
            model_name='mymodel',
            name='status_fk', 
            field=CustomAttrNameForeignKey(attr_name='status', db_column='status', to='MyStatusModel'),
        ),
        migrations.AlterField(
            model_name='historicalmymodel',
            name='status_fk',
            field=CustomAttrNameForeignKey(blank=True, db_column='status', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='MyStatusModel'),
        ),
```

**Environment (please complete the following information):**
 - OS: Ubuntu 16.04
 - Django Simple History Version: 2.1.1
 - Django Version: 1.11.6
 - Database Version: PostgreSQL 9.6.6



Discussion:
Perhaps carrying all other arguments with field_arguments [here](https://github.com/treyhunner/django-simple-history/blob/master/simple_history/models.py#L175) may fix it?
I can work on a PR if you think I'm on the right track.
@pchokron I think you're on the right track there. Feel free to work on a PR. Thanks for finding this bug

Resolving patch:
diff --git a/simple_history/models.py b/simple_history/models.py
--- a/simple_history/models.py
+++ b/simple_history/models.py
@@ -184,29 +184,22 @@ def copy_fields(self, model):
                 field.__class__ = models.IntegerField
             if isinstance(field, models.ForeignKey):
                 old_field = field
-                field_arguments = {'db_constraint': False}
+                old_swappable = old_field.swappable
+                old_field.swappable = False
+                try:
+                    _name, _path, args, field_args = old_field.deconstruct()
+                finally:
+                    old_field.swappable = old_swappable
                 if getattr(old_field, 'one_to_one', False) \
                    or isinstance(old_field, models.OneToOneField):
                     FieldType = models.ForeignKey
                 else:
                     FieldType = type(old_field)
-                if getattr(old_field, 'to_fields', []):
-                    field_arguments['to_field'] = old_field.to_fields[0]
-                if getattr(old_field, 'db_column', None):
-                    field_arguments['db_column'] = old_field.db_column
-
-                # If old_field.remote_field.model is 'self' then we have a
-                # case where object has a foreign key to itself. In this case
-                # we need to set the `model` value of the field to a model. We
-                # can use the old_field.model value.
-                if isinstance(old_field.remote_field.model, str) and \
-                   old_field.remote_field.model == 'self':
-                    object_to = old_field.model
-                else:
-                    object_to = old_field.remote_field.model
 
-                field = FieldType(
-                    object_to,
+                # Override certain arguments passed when creating the field
+                # so that they work for the historical field.
+                field_args.update(
+                    db_constraint=False,
                     related_name='+',
                     null=True,
                     blank=True,
@@ -215,7 +208,10 @@ def copy_fields(self, model):
                     serialize=True,
                     unique=False,
                     on_delete=models.DO_NOTHING,
-                    **field_arguments
+                )
+                field = FieldType(
+                    *args,
+                    **field_args
                 )
                 field.name = old_field.name
             else:


Test patch:
diff --git a/simple_history/registry_tests/migration_test_app/migrations/0002_historicalmodelwithcustomattrforeignkey_modelwithcustomattrforeignkey.py b/simple_history/registry_tests/migration_test_app/migrations/0002_historicalmodelwithcustomattrforeignkey_modelwithcustomattrforeignkey.py
new file mode 100644
--- /dev/null
+++ b/simple_history/registry_tests/migration_test_app/migrations/0002_historicalmodelwithcustomattrforeignkey_modelwithcustomattrforeignkey.py
@@ -0,0 +1,42 @@
+# Generated by Django 2.1 on 2018-10-19 21:53
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import simple_history.models
+from .. import models as my_models
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('migration_test_app', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='HistoricalModelWithCustomAttrForeignKey',
+            fields=[
+                ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
+                ('history_id', models.AutoField(primary_key=True, serialize=False)),
+                ('history_change_reason', models.CharField(max_length=100, null=True)),
+                ('history_date', models.DateTimeField()),
+                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
+                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+                ('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')),
+            ],
+            options={
+                'verbose_name': 'historical model with custom attr foreign key',
+                'ordering': ('-history_date', '-history_id'),
+                'get_latest_by': 'history_date',
+            },
+            bases=(simple_history.models.HistoricalChanges, models.Model),
+        ),
+        migrations.CreateModel(
+            name='ModelWithCustomAttrForeignKey',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('what_i_mean', my_models.CustomAttrNameForeignKey(attr_name='custom_attr_name', on_delete=django.db.models.deletion.CASCADE, to='migration_test_app.WhatIMean')),
+            ],
+        ),
+    ]
diff --git a/simple_history/registry_tests/migration_test_app/models.py b/simple_history/registry_tests/migration_test_app/models.py
--- a/simple_history/registry_tests/migration_test_app/models.py
+++ b/simple_history/registry_tests/migration_test_app/models.py
@@ -13,3 +13,31 @@ class WhatIMean(DoYouKnow):
 class Yar(models.Model):
     what = models.ForeignKey(WhatIMean, on_delete=models.CASCADE)
     history = HistoricalRecords()
+
+
+class CustomAttrNameForeignKey(models.ForeignKey):
+    def __init__(self, *args, **kwargs):
+        self.attr_name = kwargs.pop("attr_name", None)
+        super(CustomAttrNameForeignKey, self).__init__(*args, **kwargs)
+
+    def get_attname(self):
+        return self.attr_name or super(
+            CustomAttrNameForeignKey, self
+        ).get_attname()
+
+    def deconstruct(self):
+        name, path, args, kwargs = super(
+            CustomAttrNameForeignKey, self
+        ).deconstruct()
+        if self.attr_name:
+            kwargs["attr_name"] = self.attr_name
+        return name, path, args, kwargs
+
+
+class ModelWithCustomAttrForeignKey(models.Model):
+    what_i_mean = CustomAttrNameForeignKey(
+        WhatIMean,
+        models.CASCADE,
+        attr_name='custom_attr_name',
+    )
+    history = HistoricalRecords()
diff --git a/simple_history/registry_tests/tests.py b/simple_history/registry_tests/tests.py
--- a/simple_history/registry_tests/tests.py
+++ b/simple_history/registry_tests/tests.py
@@ -16,7 +16,8 @@
                             Restaurant, TrackedAbstractBaseA,
                             TrackedAbstractBaseB, TrackedWithAbstractBase,
                             TrackedWithConcreteBase, UserAccessorDefault,
-                            UserAccessorOverride, UUIDRegisterModel, Voter)
+                            UserAccessorOverride, UUIDRegisterModel, Voter,
+                            ModelWithCustomAttrForeignKey)
 
 get_model = apps.get_model
 User = get_user_model()
@@ -109,15 +110,15 @@ def test_tracked_abstract_base(self):
 
     def test_tracked_concrete_base(self):
         self.assertEqual(
-            [
+            sorted(
                 f.attname
                 for f in TrackedWithConcreteBase.history.model._meta.fields
-            ],
-            [
+            ),
+            sorted([
                 'id', 'trackedconcretebase_ptr_id', 'history_id',
                 'history_change_reason', 'history_date', 'history_user_id',
                 'history_type',
-            ],
+            ]),
         )
 
     def test_multiple_tracked_bases(self):
@@ -128,32 +129,38 @@ class TrackedWithMultipleAbstractBases(
 
     def test_tracked_abstract_and_untracked_concrete_base(self):
         self.assertEqual(
-            [f.attname for f in InheritTracking1.history.model._meta.fields],
-            [
+            sorted(
+                f.attname for f in InheritTracking1.history.model._meta.fields
+            ),
+            sorted([
                 'id', 'untrackedconcretebase_ptr_id', 'history_id',
                 'history_change_reason', 'history_date',
                 'history_user_id', 'history_type',
-            ],
+            ]),
         )
 
     def test_indirect_tracked_abstract_base(self):
         self.assertEqual(
-            [f.attname for f in InheritTracking2.history.model._meta.fields],
-            [
+            sorted(
+                f.attname for f in InheritTracking2.history.model._meta.fields
+            ),
+            sorted([
                 'id', 'baseinherittracking2_ptr_id', 'history_id',
                 'history_change_reason', 'history_date',
                 'history_user_id', 'history_type',
-            ],
+            ]),
         )
 
     def test_indirect_tracked_concrete_base(self):
         self.assertEqual(
-            [f.attname for f in InheritTracking3.history.model._meta.fields],
-            [
+            sorted(
+                f.attname for f in InheritTracking3.history.model._meta.fields
+            ),
+            sorted([
                 'id', 'baseinherittracking3_ptr_id', 'history_id',
                 'history_change_reason', 'history_date',
                 'history_user_id', 'history_type',
-            ],
+            ]),
         )
 
     def test_registering_with_tracked_abstract_base(self):
@@ -161,6 +168,16 @@ def test_registering_with_tracked_abstract_base(self):
             register(InheritTracking4)
 
 
+class TestCustomAttrForeignKey(TestCase):
+    """ https://github.com/treyhunner/django-simple-history/issues/431 """
+
+    def test_custom_attr(self):
+        field = ModelWithCustomAttrForeignKey.history.model._meta.get_field(
+            'poll',
+        )
+        self.assertEqual(field.attr_name, 'custom_poll')
+
+
 class TestMigrate(TestCase):
 
     def test_makemigration_command(self):
diff --git a/simple_history/tests/models.py b/simple_history/tests/models.py
--- a/simple_history/tests/models.py
+++ b/simple_history/tests/models.py
@@ -59,6 +59,34 @@ def get_absolute_url(self):
         return reverse('poll-detail', kwargs={'pk': self.pk})
 
 
+class CustomAttrNameForeignKey(models.ForeignKey):
+    def __init__(self, *args, **kwargs):
+        self.attr_name = kwargs.pop("attr_name", None)
+        super(CustomAttrNameForeignKey, self).__init__(*args, **kwargs)
+
+    def get_attname(self):
+        return self.attr_name or super(
+            CustomAttrNameForeignKey, self
+        ).get_attname()
+
+    def deconstruct(self):
+        name, path, args, kwargs = super(
+            CustomAttrNameForeignKey, self
+        ).deconstruct()
+        if self.attr_name:
+            kwargs["attr_name"] = self.attr_name
+        return name, path, args, kwargs
+
+
+class ModelWithCustomAttrForeignKey(models.Model):
+    poll = CustomAttrNameForeignKey(
+        Poll,
+        models.CASCADE,
+        attr_name='custom_poll',
+    )
+    history = HistoricalRecords()
+
+
 class Temperature(models.Model):
     location = models.CharField(max_length=200)
     temperature = models.IntegerField()