Repository: jazzband/django-simple-history

Issue:
Issue with CustomForeignKey - 1.5.0
I am just getting up to speed with django (two months experience) and am trying to get your amazing library working (tdlr: take this well salted since I am a noob):

I am getting this error when I try to run makemigrations (running django 1.7)

```
File "/Users/thekgf/CentriFile/server/venv/lib/python2.7/site-packages/django/utils/module_loading.py", line 29, in import_string:  return getattr(module, class_name)
ImportError: Module "simple_history.models.CustomForeignKey" does not define a "CustomForeignKey" attribute/class
```

Reading through open issues, I've tried both approaches register(Org) and the standard history = HistoricalRecords() approach and they both give the same result.

I have a  object Org that has a foreign key to a Human object, that inherits from ContactInfo entity.

``` python
class Org(ContactInfoEntity):
    # Commenting the following out removes the error.
    owner = models.ForeignKey(Human, related_name='org_owner', null=True)
    # this works however:
    # owner = models.ForeignKey(ContactInfoEntity, related_name='org_owner', null=True)

class Human(ContactInfoEntity):
    ... a fewCharFields but even with no fields in here I get the error

class ContactInfoEntity(TimeStampedModel):
     ... a few CharFields and ForeignKey fields but even with no fields in here I get the error

class TimeStampedModel(models.Model):
    created = CreationDateTimeField(_('created'))
    modified = ModificationDateTimeField(_('modified'))

    class Meta:
        get_latest_by = 'modified'
        ordering = ('-modified', '-created',)
        abstract = True
```

The only reference I can see to CustomForeignKey in my simple_history package is:

``` python
def get_custom_fk_class(parent_type):
    return type(str('CustomForeignKey'), (ForeignKeyMixin, parent_type), {})
```

But, I figure something... perhaps another package I have installed may be messing with this?

Apologies if this is a RTFM error, but I've gone over the docs quite diligently and am not seeing a solution.

PS: 

```
Django==1.7
Pillow==2.5.3
PyJWT==0.2.1
Unidecode==0.04.16
coverage==3.7.1
django-autoslug==1.7.2
django-extensions==1.4.0
django-image-tools==0.8.b3
django-simple-history==1.5.0
djangorestframework==2.4.2
djangorestframework-jwt==1.0.1
enum34==1.0
psycopg2==2.5.4
six==1.8.0
wsgiref==0.1.2
```



Discussion:
I am starting from a base project with the poll example to attempt to figure out where the issue is coming in. Will comment here if I find anything out.

It appears if I mark  all classes above Human with

``` python
    class Meta:
        abstract = True
```

it stops giving that error (I am working out of a smaller test project now), but this isn't an option for me in my main project.  I am going to see if I can figure out how to debug it and get some more useful info for you guys. :)

This sounds really similar to #120. At the very least we should pin down what's causing it, and document a limitation if that is the case.

Thanks for pointing me to that issue. He definitely was getting the exact same error as me. In my simple test case that's failing I am not using multiple inheritance, but ... I will look to all the advice you gave him and see if there's any of it that I can use to help me. Thanks! I will write back with what I find, and hopefully will try to get some insight into what exactly is going wrong.  Also, if you want I can provide the code I am testing with to repro the issue. It's simple and illustrates the issue quickly.

Looking at it more, it looks like the issue lies in the new migration code. Fields are expected to be serializable into something that include a dotted string representing an importable definition of the field type. `CustomForeignKey` is a hack thrown together with `type()`, so that's not an option the way things are written now. I'll see if I can figure out something that'll work.

The method that is generating the path to `CustomForeignKey`: https://github.com/django/django/blob/1.7c3/django/db/models/fields/__init__.py#L292

Awesome, thank you. I'm still getting to know the django-simple-history code, but if I think of anything I'll let you know.


Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,12 @@
 from setuptools import setup
 import simple_history
 
+tests_require = ["Django>=1.4", "webtest==2.0.6", "django-webtest==1.7"]
+try:
+    from unittest import skipUnless
+except ImportError:  # Python 2.6 compatibility
+    tests_require.append("unittest2")
+
 setup(
     name='django-simple-history',
     version=simple_history.__version__,
@@ -27,7 +33,7 @@
         'Programming Language :: Python :: 3.3',
         "License :: OSI Approved :: BSD License",
     ],
-    tests_require=["Django>=1.4", "webtest==2.0.6", "django-webtest==1.7"],
+    tests_require=tests_require,
     include_package_data=True,
     test_suite='runtests.main',
 )
diff --git a/simple_history/models.py b/simple_history/models.py
--- a/simple_history/models.py
+++ b/simple_history/models.py
@@ -147,7 +147,7 @@ def copy_fields(self, model):
                 # Don't allow reverse relations.
                 # ForeignKey knows best what datatype to use for the column
                 # we'll used that as soon as it's finalized by copying rel.to
-                field.__class__ = get_custom_fk_class(type(field))
+                field.__class__ = CustomForeignKeyField
                 field.rel.related_name = '+'
                 field.null = True
                 field.blank = True
@@ -240,7 +240,8 @@ def get_history_user(self, instance):
                 return None
 
 
-class ForeignKeyMixin(object):
+class CustomForeignKeyField(models.ForeignKey):
+
     def get_attname(self):
         return self.name
 
@@ -300,7 +301,13 @@ def get_field(self, other, cls):
     def do_related_class(self, other, cls):
         field = self.get_field(other, cls)
         if not hasattr(self, 'related'):
-            self.related = RelatedObject(other, cls.instance_type, self)
+            try:
+                instance_type = cls.instance_type
+            except AttributeError:  # when model is reconstituted for migration
+                if cls.__module__ != "__fake__":  # not from migrations, error
+                    raise
+            else:
+                self.related = RelatedObject(other, instance_type, self)
         transform_field(field)
         field.rel = None
 
@@ -309,10 +316,6 @@ def contribute_to_class(self, cls, name):
         RelatedField.contribute_to_class(self, cls, name)
 
 
-def get_custom_fk_class(parent_type):
-    return type(str('CustomForeignKey'), (ForeignKeyMixin, parent_type), {})
-
-
 def transform_field(field):
     """Customize field appropriately for use in historical model"""
     field.name = field.attname


Test patch:
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
@@ -193,3 +193,8 @@ class UnicodeVerboseName(models.Model):
 
     class Meta:
         verbose_name = '\u570b'
+
+
+class CustomFKError(models.Model):
+    fk = models.ForeignKey(SecondLevelInheritedModel)
+    history = HistoricalRecords()
diff --git a/simple_history/tests/tests/test_commands.py b/simple_history/tests/tests/test_commands.py
--- a/simple_history/tests/tests/test_commands.py
+++ b/simple_history/tests/tests/test_commands.py
@@ -1,6 +1,11 @@
 from contextlib import contextmanager
 from six.moves import cStringIO as StringIO
 from datetime import datetime
+try:
+    from unittest import skipUnless
+except ImportError:
+    from unittest2 import skipUnless
+import django
 from django.test import TestCase
 from django.core import management
 from simple_history import models as sh_models
@@ -101,3 +106,10 @@ def test_no_historical(self):
                                     stdout=out)
         self.assertIn(populate_history.Command.NO_REGISTERED_MODELS,
                       out.getvalue())
+
+
+class TestMigrate(TestCase):
+
+    @skipUnless(django.get_version() >= "1.7", "Requires 1.7 migrations")
+    def test_migrate_command(self):
+        management.call_command('migrate', fake=True, stdout=StringIO())
diff --git a/simple_history/tests/tests/test_models.py b/simple_history/tests/tests/test_models.py
--- a/simple_history/tests/tests/test_models.py
+++ b/simple_history/tests/tests/test_models.py
@@ -1,7 +1,12 @@
 from __future__ import unicode_literals
 
 from datetime import datetime, timedelta
+try:
+    from unittest import skipUnless
+except ImportError:
+    from unittest2 import skipUnless
 
+import django
 try:
     from django.contrib.auth import get_user_model
     User = get_user_model()
@@ -18,7 +23,7 @@
     FileModel, Document, Book, HistoricalPoll, Library, State, AbstractBase,
     ConcreteAttr, ConcreteUtil, SelfFK, Temperature, WaterLevel,
     ExternalModel1, ExternalModel3, UnicodeVerboseName, HistoricalChoice,
-    HistoricalState
+    HistoricalState, HistoricalCustomFKError
 )
 from ..external.models import ExternalModel2, ExternalModel4
 
@@ -469,3 +474,8 @@ def test_import_related(self):
     def test_string_related(self):
         field_object = HistoricalState._meta.get_field_by_name('library_id')[0]
         self.assertEqual(field_object.related.model, State)
+
+    @skipUnless(django.get_version() >= "1.7", "Requires 1.7 migrations")
+    def test_state_serialization_of_customfk(self):
+        from django.db.migrations import state
+        state.ModelState.from_model(HistoricalCustomFKError)