diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 76fc42d368..90226221be 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -151,6 +151,23 @@ class ModelState(object): options[name] = set(normalize_together(it)) else: options[name] = model._meta.original_attrs[name] + + def flatten_bases(model): + bases = [] + for base in model.__bases__: + if hasattr(base, "_meta") and base._meta.abstract: + bases.extend(flatten_bases(base)) + else: + bases.append(base) + return bases + + # We can't rely on __mro__ directly because we only want to flatten + # abstract models and not the whole tree. However by recursing on + # __bases__ we may end up with duplicates and ordering issues, we + # therefore discard any duplicates and reorder the bases according + # to their index in the MRO. + flattened_bases = sorted(set(flatten_bases(model)), key=lambda x:model.__mro__.index(x)) + # Make our record bases = tuple( ( @@ -158,12 +175,11 @@ class ModelState(object): if hasattr(base, "_meta") else base ) - for base in model.__bases__ - if (not hasattr(base, "_meta") or not base._meta.abstract) + for base in flattened_bases ) # Ensure at least one base inherits from models.Model if not any((isinstance(base, six.string_types) or issubclass(base, models.Model)) for base in bases): - bases = (models.Model, ) + bases = (models.Model,) return cls( model._meta.app_label, model._meta.object_name, diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 51c95afd96..debfc760b5 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -14,7 +14,7 @@ class OperationTests(MigrationTestBase): both forwards and backwards. """ - def set_up_test_model(self, app_label, second_model=False, related_model=False): + def set_up_test_model(self, app_label, second_model=False, related_model=False, mti_model=False): """ Creates a test model state and database table. """ @@ -38,7 +38,12 @@ class OperationTests(MigrationTestBase): ], )] if second_model: - operations.append(migrations.CreateModel("Stable", [("id", models.AutoField(primary_key=True))])) + operations.append(migrations.CreateModel( + "Stable", + [ + ("id", models.AutoField(primary_key=True)), + ] + )) if related_model: operations.append(migrations.CreateModel( "Rider", @@ -47,6 +52,21 @@ class OperationTests(MigrationTestBase): ("pony", models.ForeignKey("Pony")), ], )) + if mti_model: + operations.append(migrations.CreateModel( + "ShetlandPony", + fields=[ + ('pony_ptr', models.OneToOneField( + auto_created=True, + primary_key=True, + to_field='id', + serialize=False, + to='Pony', + )), + ("cuteness", models.IntegerField(default=1)), + ], + bases=['%s.Pony' % app_label], + )) project_state = ProjectState() for operation in operations: operation.state_forwards(app_label, project_state) @@ -495,7 +515,7 @@ class OperationTests(MigrationTestBase): Tests the RunPython operation """ - project_state = self.set_up_test_model("test_runpython") + project_state = self.set_up_test_model("test_runpython", mti_model=True) # Create the operation def inner_method(models, schema_editor): @@ -533,7 +553,34 @@ class OperationTests(MigrationTestBase): no_reverse_operation.database_forwards("test_runpython", editor, project_state, new_state) with self.assertRaises(NotImplementedError): no_reverse_operation.database_backwards("test_runpython", editor, new_state, project_state) + self.assertEqual(project_state.render().get_model("test_runpython", "Pony").objects.count(), 2) + def create_ponies(models, schema_editor): + Pony = models.get_model("test_runpython", "Pony") + pony1 = Pony.objects.create(pink=1, weight=3.55) + self.assertIsNot(pony1.pk, None) + pony2 = Pony.objects.create(weight=5) + self.assertIsNot(pony2.pk, None) + self.assertNotEqual(pony1.pk, pony2.pk) + + operation = migrations.RunPython(create_ponies) + with connection.schema_editor() as editor: + operation.database_forwards("test_runpython", editor, project_state, new_state) + self.assertEqual(project_state.render().get_model("test_runpython", "Pony").objects.count(), 4) + + def create_shetlandponies(models, schema_editor): + ShetlandPony = models.get_model("test_runpython", "ShetlandPony") + pony1 = ShetlandPony.objects.create(weight=4.0) + self.assertIsNot(pony1.pk, None) + pony2 = ShetlandPony.objects.create(weight=5.0) + self.assertIsNot(pony2.pk, None) + self.assertNotEqual(pony1.pk, pony2.pk) + + operation = migrations.RunPython(create_shetlandponies) + with connection.schema_editor() as editor: + operation.database_forwards("test_runpython", editor, project_state, new_state) + self.assertEqual(project_state.render().get_model("test_runpython", "Pony").objects.count(), 6) + self.assertEqual(project_state.render().get_model("test_runpython", "ShetlandPony").objects.count(), 2) class MigrateNothingRouter(object): """ diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 187b06b94a..10f5e7d9ab 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -166,6 +166,16 @@ class StateTests(TestCase): app_label = "migrations" apps = Apps() + class AbstractSubFooBar(FooBar): + class Meta: + abstract = True + apps = Apps() + + class SubFooBar(AbstractSubFooBar): + class Meta: + app_label = "migrations" + apps = Apps() + apps = Apps(["migrations"]) # We shouldn't be able to render yet @@ -175,8 +185,13 @@ class StateTests(TestCase): # Once the parent models are in the app registry, it should be fine ModelState.from_model(Foo).render(apps) + self.assertSequenceEqual(ModelState.from_model(Foo).bases, [models.Model]) ModelState.from_model(Bar).render(apps) + self.assertSequenceEqual(ModelState.from_model(Bar).bases, [models.Model]) ModelState.from_model(FooBar).render(apps) + self.assertSequenceEqual(ModelState.from_model(FooBar).bases, ['migrations.foo', 'migrations.bar']) + ModelState.from_model(SubFooBar).render(apps) + self.assertSequenceEqual(ModelState.from_model(SubFooBar).bases, ['migrations.foobar']) def test_render_project_dependencies(self): """