Repository: jazzband/django-simple-history

Issue:
RelatedObject replaced with ForeignObjectRel

Django 1.8: ImportError: No module named related
```
  File "/Users/thekgf/CentriFile/allay/venv/lib/python2.7/site-packages/simple_history/models.py", line 13, in <module>
    from django.db.models.related import RelatedObject
ImportError: No module named related
```

Django 1.8 has made some changes here and removed the RelatedObject API. More details are here: https://docs.djangoproject.com/en/dev/releases/1.8/#model-attribute-on-private-model-relations



Discussion:

Yeah. I've looked a little at it, but I haven't figured out what the solution is yet. If anyone else would like to try, please feel free! @treyhunner and I will both be at PyCon in Montreal in April as well. We talked a little about doing some sprints while we're there. This may be something we work on then if it isn't fixed by then.


Resolving patch:
diff --git a/simple_history/models.py b/simple_history/models.py
--- a/simple_history/models.py
+++ b/simple_history/models.py
@@ -2,15 +2,12 @@
 
 import threading
 import copy
-try:
-    from django.apps import apps
-except ImportError:  # Django < 1.7
-    apps = None
+import warnings
+
 from django.db import models, router
 from django.db.models import loading
 from django.db.models.fields.proxy import OrderWrt
 from django.db.models.fields.related import RelatedField
-from django.db.models.related import RelatedObject
 from django.conf import settings
 from django.contrib import admin
 from django.utils import importlib, six
@@ -18,6 +15,13 @@
 from django.utils.encoding import smart_text
 from django.utils.timezone import now
 from django.utils.translation import string_concat
+
+from .manager import HistoryDescriptor
+
+try:
+    from django.apps import apps
+except ImportError:  # Django < 1.7
+    apps = None
 try:
     from south.modelsinspector import add_introspection_rules
 except ImportError:  # south not present
@@ -25,7 +29,6 @@
 else:  # south configuration for CustomForeignKeyField
     add_introspection_rules(
         [], ["^simple_history.models.CustomForeignKeyField"])
-from .manager import HistoryDescriptor
 
 registered_models = {}
 
@@ -121,18 +124,25 @@ def copy_fields(self, model):
         for field in model._meta.fields:
             field = copy.copy(field)
             field.rel = copy.copy(field.rel)
-            if isinstance(field, models.ForeignKey):
-                # 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__ = CustomForeignKeyField
-                field.rel.related_name = '+'
-                field.null = True
-                field.blank = True
             if isinstance(field, OrderWrt):
                 # OrderWrt is a proxy field, switch to a plain IntegerField
                 field.__class__ = models.IntegerField
-            transform_field(field)
+            if isinstance(field, models.ForeignKey):
+                old_field = field
+                field = type(field)(
+                    field.rel.to,
+                    related_name='+',
+                    null=True,
+                    blank=True,
+                    primary_key=False,
+                    db_index=True,
+                    serialize=True,
+                )
+                field._unique = False
+                field.name = old_field.name
+                field.db_constraint = False
+            else:
+                transform_field(field)
             fields[field.name] = field
         return fields
 
@@ -227,6 +237,8 @@ def get_history_user(self, instance):
 class CustomForeignKeyField(models.ForeignKey):
 
     def __init__(self, *args, **kwargs):
+        warnings.warn("CustomForeignKeyField is deprecated.",
+                      DeprecationWarning)
         super(CustomForeignKeyField, self).__init__(*args, **kwargs)
         self.db_constraint = False
         self.generate_reverse_relation = False
@@ -289,13 +301,6 @@ def get_field(self, other, cls):
 
     def do_related_class(self, other, cls):
         field = self.get_field(other, cls)
-        if not hasattr(self, 'related'):
-            try:
-                instance_type = cls.instance_type
-            except AttributeError:  # when model is reconstituted for migration
-                pass  # happens during migrations
-            else:
-                self.related = RelatedObject(other, instance_type, self)
         transform_field(field)
         field.rel = None
 


Test patch:
diff --git a/simple_history/tests/migration_test_app/migrations/0001_initial.py b/simple_history/tests/migration_test_app/migrations/0001_initial.py
--- a/simple_history/tests/migration_test_app/migrations/0001_initial.py
+++ b/simple_history/tests/migration_test_app/migrations/0001_initial.py
@@ -3,7 +3,7 @@
 from __future__ import unicode_literals
 
 from django.db import models, migrations
-import simple_history.models
+import django.db.models.deletion
 from django.conf import settings
 
 
@@ -30,12 +30,9 @@ class Migration(migrations.Migration):
                 ('history_id', models.AutoField(serialize=False, primary_key=True)),
                 ('history_date', models.DateTimeField()),
                 ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
-                ('history_user', models.ForeignKey(null=True, to=settings.AUTH_USER_MODEL)),
+                ('history_user', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
             ],
-            options={
-                'verbose_name': 'historical yar',
-                'ordering': ('-history_date', '-history_id'),
-            },
+            options={'ordering': ('-history_date', '-history_id'), 'get_latest_by': 'history_date', 'verbose_name': 'historical yar'},
             bases=(models.Model,),
         ),
         migrations.CreateModel(
@@ -59,8 +56,8 @@ class Migration(migrations.Migration):
         ),
         migrations.AddField(
             model_name='historicalyar',
-            name='what_id',
-            field=simple_history.models.CustomForeignKeyField(to='migration_test_app.WhatIMean', blank=True, null=True, related_name='+'),
+            name='what',
+            field=models.ForeignKey(related_name='+', db_constraint=False, blank=True, to='migration_test_app.WhatIMean', null=True),
             preserve_default=True,
         ),
     ]
diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py
--- a/simple_history/tests/tests/test_admin.py
+++ b/simple_history/tests/tests/test_admin.py
@@ -234,4 +234,3 @@ def test_deleteting_user(self):
 
         historical_poll = poll.history.all()[0]
         self.assertEqual(historical_poll.history_user, None)
-
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,17 +1,9 @@
 from __future__ import unicode_literals
 
 from datetime import datetime, timedelta
-try:
-    from unittest import skipUnless
-except ImportError:
-    from unittest2 import skipUnless
+import unittest
 
 import django
-try:
-    from django.contrib.auth import get_user_model
-    User = get_user_model()
-except ImportError:  # django 1.4 compatibility
-    from django.contrib.auth.models import User
 from django.db import models
 from django.db.models.loading import get_model
 from django.db.models.fields.proxy import OrderWrt
@@ -29,6 +21,16 @@
 )
 from ..external.models import ExternalModel2, ExternalModel4
 
+try:
+    from unittest import skipUnless
+except ImportError:
+    from unittest2 import skipUnless
+try:
+    from django.contrib.auth import get_user_model
+    User = get_user_model()
+except ImportError:  # django 1.4 compatibility
+    from django.contrib.auth.models import User
+
 today = datetime(2021, 1, 1, 10, 0)
 tomorrow = today + timedelta(days=1)
 yesterday = today - timedelta(days=1)
@@ -324,7 +326,7 @@ def test_register_custome_records(self):
         self.assertEqual(expected, str(voter.history.all()[0])[:len(expected)])
 
 
-class CreateHistoryModelTests(TestCase):
+class CreateHistoryModelTests(unittest.TestCase):
 
     def test_create_history_model_with_one_to_one_field_to_integer_field(self):
         records = HistoricalRecords()
@@ -487,12 +489,20 @@ def test_invalid_bases(self):
             self.assertRaises(TypeError, HistoricalRecords, bases=bases)
 
     def test_import_related(self):
-        field_object = HistoricalChoice._meta.get_field_by_name('poll_id')[0]
-        self.assertEqual(field_object.related.model, Choice)
+        field_object = HistoricalChoice._meta.get_field_by_name('poll')[0]
+        try:
+            related_model = field_object.rel.related_model
+        except AttributeError:  # Django<1.8
+            related_model = field_object.related.model
+        self.assertEqual(related_model, HistoricalChoice)
 
     def test_string_related(self):
-        field_object = HistoricalState._meta.get_field_by_name('library_id')[0]
-        self.assertEqual(field_object.related.model, State)
+        field_object = HistoricalState._meta.get_field_by_name('library')[0]
+        try:
+            related_model = field_object.rel.related_model
+        except AttributeError:  # Django<1.8
+            related_model = field_object.related.model
+        self.assertEqual(related_model, HistoricalState)
 
     @skipUnless(django.get_version() >= "1.7", "Requires 1.7 migrations")
     def test_state_serialization_of_customfk(self):
@@ -652,7 +662,8 @@ class TestLatest(TestCase):
     """"Test behavior of `latest()` without any field parameters"""
 
     def setUp(self):
-        poll = Poll.objects.create(question="Does `latest()` work?", pub_date=yesterday)
+        poll = Poll.objects.create(
+            question="Does `latest()` work?", pub_date=yesterday)
         poll.pub_date = today
         poll.save()