Repository: jazzband/django-simple-history

Issue:
Django 1.7 migration problem with Meta.order_with_respect_to
A model with the `Meta.order_with_respect_to` option result in History models with broken migrations.

The model Meta option [order_with_respect_to](https://docs.djangoproject.com/en/dev/ref/models/options/#order-with-respect-to) adds a field `_order`, which is implemented as [OrderWrt](https://github.com/django/django/blob/master/django/db/models/fields/proxy.py), which derives from IntegerField.

In Django 1.6, and in Django 1.7 without migrations, this field appears as an `OrderWrt` field.  When creating an app migration, Django performs special handling for this field.  It does not appear in the field list for the `migrations.CreateModel` call, but instead is applied with a special function `migrations.AlterOrderWithRespectTo`.  The field does not appear in the `CreateModel` call for the history model.  When the migrated app is loaded (tests, run server, etc.), Django attempts to access the `_order` field on the History model, resulting in errors.

I fixed this in [my code](https://github.com/jwhitlock/web-platform-compat/blob/40cb7f4ee5c82cb75b10accc0f1dee0f1289d978/webplatformcompat/models.py#L276) with a custom `HistoricalRecords`:

```
class HistoricalRecordsWithOWRT(HistoricalRecords):
    """Generate history for models with order_with_respect_to."""
    def copy_fields(self, model):
        fields = super(HistoricalRecordsWithOWRT, self).copy_fields(model)
        name, path, args, kwargs = fields['_order'].deconstruct()
        fields['_order'] = models.IntegerField(*args, **kwargs)
        return fields
```

This could be generalized, in a similar way to the `ForeignKey` processing in [copy_fields](https://github.com/treyhunner/django-simple-history/blob/master/simple_history/models.py#L130):

```
def copy_fields(self, model):
        """
        Creates copies of the model's original fields, returning
        a dictionary mapping field name to copied field object.
        """
        fields = {}
        for field in model._meta.fields:
            ...
            if isinstance(field, models.OrderWrt):
                # Do something...
        return fields
```

I could see a few options:
1. Replace it with a plain `IntegerField`, which will appear as normal in the migration
2. Drop the field, since writing the old value to `_order` won't actually change the order
3. Create a `CustomOrderWrt` that does try to change the order when restoring from history.

I'd like some feedback on these options before I write a pull request.  I'm also not quite sure how to test this.  Look for a field in the migration?  Check the types of fields? 



Discussion:
I think should stay away from custom fields as much as possible. `CustomForeignKeyField` has already caused issues, but it is handling some behavior that was added in 1.4.0.

Of the other two options, I like the second if it'll work. I think it makes sense for historical records to remain ordered by the auto incremented ID.


Resolving patch:
diff --git a/simple_history/models.py b/simple_history/models.py
--- a/simple_history/models.py
+++ b/simple_history/models.py
@@ -8,6 +8,7 @@
     apps = None
 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
@@ -135,6 +136,9 @@ def copy_fields(self, model):
                 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)
             fields[field.name] = field
         return fields


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
@@ -217,3 +217,18 @@ class Meta:
 class CustomFKError(models.Model):
     fk = models.ForeignKey(SecondLevelInheritedModel)
     history = HistoricalRecords()
+
+
+class Series(models.Model):
+    """A series of works, like a trilogy of books."""
+    name = models.CharField(max_length=100)
+    author = models.CharField(max_length=100)
+
+
+class SeriesWork(models.Model):
+    series = models.ForeignKey('Series', related_name='works')
+    title = models.CharField(max_length=100)
+    history = HistoricalRecords()
+
+    class Meta:
+        order_with_respect_to = 'series'
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
@@ -14,6 +14,7 @@
     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
 from django.test import TestCase
 from django.core.files.base import ContentFile
 
@@ -24,7 +25,7 @@
     Person, FileModel, Document, Book, HistoricalPoll, Library, State,
     AbstractBase, ConcreteAttr, ConcreteUtil, SelfFK, Temperature, WaterLevel,
     ExternalModel1, ExternalModel3, UnicodeVerboseName, HistoricalChoice,
-    HistoricalState, HistoricalCustomFKError
+    HistoricalState, HistoricalCustomFKError, Series, SeriesWork
 )
 from ..external.models import ExternalModel2, ExternalModel4
 
@@ -519,3 +520,122 @@ def test_non_relational(self):
         with self.settings(DATABASES={'default': {
                 'ENGINE': 'django_mongodb_engine'}}):
             assert convert_auto_field(self.field) == models.TextField
+
+
+class TestOrderWrtField(TestCase):
+    """Check behaviour of _order field added by Meta.order_with_respect_to.
+
+    The Meta.order_with_respect_to option adds an OrderWrt field named
+    "_order", where OrderWrt is a proxy class for an IntegerField that sets
+    some default options.
+
+    The simple_history strategy is:
+    - Convert to a plain IntegerField in the historical record
+    - When restoring a historical instance, add the old value.  This may
+      result in duplicate ordering values and non-deterministic ordering.
+    """
+
+    def setUp(self):
+        """Create works in published order."""
+        s = self.series = Series.objects.create(
+            name="The Chronicles of Narnia", author="C.S. Lewis")
+        self.w_lion = s.works.create(
+            title="The Lion, the Witch and the Wardrobe")
+        self.w_caspian = s.works.create(title="Prince Caspian")
+        self.w_voyage = s.works.create(title="The Voyage of the Dawn Treader")
+        self.w_chair = s.works.create(title="The Silver Chair")
+        self.w_horse = s.works.create(title="The Horse and His Boy")
+        self.w_nephew = s.works.create(title="The Magician's Nephew")
+        self.w_battle = s.works.create(title="The Last Battle")
+
+    def test_order(self):
+        """Confirm that works are ordered by creation."""
+        order = self.series.get_serieswork_order()
+        expected = [
+            self.w_lion.pk,
+            self.w_caspian.pk,
+            self.w_voyage.pk,
+            self.w_chair.pk,
+            self.w_horse.pk,
+            self.w_nephew.pk,
+            self.w_battle.pk]
+        self.assertEqual(order, expected)
+        self.assertEqual(0, self.w_lion._order)
+        self.assertEqual(1, self.w_caspian._order)
+        self.assertEqual(2, self.w_voyage._order)
+        self.assertEqual(3, self.w_chair._order)
+        self.assertEqual(4, self.w_horse._order)
+        self.assertEqual(5, self.w_nephew._order)
+        self.assertEqual(6, self.w_battle._order)
+
+    def test_order_field_in_historical_model(self):
+        work_order_field = self.w_lion._meta.get_field_by_name('_order')[0]
+        self.assertEqual(type(work_order_field), OrderWrt)
+
+        history = self.w_lion.history.all()[0]
+        history_order_field = history._meta.get_field_by_name('_order')[0]
+        self.assertEqual(type(history_order_field), models.IntegerField)
+
+    def test_history_object_has_order(self):
+        history = self.w_lion.history.all()[0]
+        self.assertEqual(self.w_lion._order, history.history_object._order)
+
+    def test_restore_object_with_changed_order(self):
+        # Change a title
+        self.w_caspian.title = 'Prince Caspian: The Return to Narnia'
+        self.w_caspian.save()
+        self.assertEqual(2, len(self.w_caspian.history.all()))
+        self.assertEqual(1, self.w_caspian._order)
+
+        # Switch to internal chronological order
+        chronological = [
+            self.w_nephew.pk,
+            self.w_lion.pk,
+            self.w_horse.pk,
+            self.w_caspian.pk,
+            self.w_voyage.pk,
+            self.w_chair.pk,
+            self.w_battle.pk]
+        self.series.set_serieswork_order(chronological)
+        self.assertEqual(self.series.get_serieswork_order(), chronological)
+
+        # This uses an update, not a save, so no new history is created
+        w_caspian = SeriesWork.objects.get(id=self.w_caspian.id)
+        self.assertEqual(2, len(w_caspian.history.all()))
+        self.assertEqual(1, w_caspian.history.all()[0]._order)
+        self.assertEqual(1, w_caspian.history.all()[1]._order)
+        self.assertEqual(3, w_caspian._order)
+
+        # Revert to first title, old order
+        old = w_caspian.history.all()[1].history_object
+        old.save()
+        w_caspian = SeriesWork.objects.get(id=self.w_caspian.id)
+        self.assertEqual(3, len(w_caspian.history.all()))
+        self.assertEqual(1, w_caspian.history.all()[0]._order)
+        self.assertEqual(1, w_caspian.history.all()[1]._order)
+        self.assertEqual(1, w_caspian.history.all()[2]._order)
+        self.assertEqual(1, w_caspian._order)  # The order changed
+        w_lion = SeriesWork.objects.get(id=self.w_lion.id)
+        self.assertEqual(1, w_lion._order)  # and is identical to another order
+
+        # New order is non-deterministic around identical IDs
+        series = Series.objects.get(id=self.series.id)
+        order = series.get_serieswork_order()
+        self.assertEqual(order[0], self.w_nephew.pk)
+        self.assertTrue(order[1] in (self.w_lion.pk, self.w_caspian.pk))
+        self.assertTrue(order[2] in (self.w_lion.pk, self.w_caspian.pk))
+        self.assertEqual(order[3], self.w_horse.pk)
+        self.assertEqual(order[4], self.w_voyage.pk)
+        self.assertEqual(order[5], self.w_chair.pk)
+        self.assertEqual(order[6], self.w_battle.pk)
+
+    @skipUnless(django.get_version() >= "1.7", "Requires 1.7 migrations")
+    def test_migrations_include_order(self):
+        from django.db.migrations import state
+        model_state = state.ModelState.from_model(SeriesWork.history.model)
+        found = False
+        for name, field in model_state.fields:
+            if name == '_order':
+                found = True
+                self.assertEqual(type(field), models.IntegerField)
+        assert found, '_order not in fields ' + repr(model_state.fields)