From 7b17350a1bfa527358e722baf962a5f9cfb838ea Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 12 Jun 2014 10:21:26 -0700 Subject: [PATCH] Fixed #22823 (and partly #22563) - FKs from unmigrated apps breaking state. Thanks to bendavis78 for the test and diagnostic work. --- django/db/migrations/state.py | 31 +++++++++++-------- .../migrated_app/__init__.py | 0 .../migrated_app/migrations/0001_initial.py | 30 ++++++++++++++++++ .../migrated_app/migrations/__init__.py | 0 .../migrated_app/models.py | 0 .../migrated_unapplied_app/__init__.py | 0 .../migrations/0001_initial.py | 22 +++++++++++++ .../migrations/__init__.py | 0 .../migrated_unapplied_app/models.py | 12 +++++++ .../unmigrated_app/__init__.py | 0 .../unmigrated_app/models.py | 9 ++++++ tests/migrations/test_commands.py | 23 ++++++++++++++ 12 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 tests/migrations/migrations_test_apps/migrated_app/__init__.py create mode 100644 tests/migrations/migrations_test_apps/migrated_app/migrations/0001_initial.py create mode 100644 tests/migrations/migrations_test_apps/migrated_app/migrations/__init__.py create mode 100644 tests/migrations/migrations_test_apps/migrated_app/models.py create mode 100644 tests/migrations/migrations_test_apps/migrated_unapplied_app/__init__.py create mode 100644 tests/migrations/migrations_test_apps/migrated_unapplied_app/migrations/0001_initial.py create mode 100644 tests/migrations/migrations_test_apps/migrated_unapplied_app/migrations/__init__.py create mode 100644 tests/migrations/migrations_test_apps/migrated_unapplied_app/models.py create mode 100644 tests/migrations/migrations_test_apps/unmigrated_app/__init__.py create mode 100644 tests/migrations/migrations_test_apps/unmigrated_app/models.py diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 40bb279cfc..750383c39f 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -44,11 +44,13 @@ class ProjectState(object): # Any apps in self.real_apps should have all their models included # in the render. We don't use the original model instances as there # are some variables that refer to the Apps object. + # FKs/M2Ms from real apps are also not included as they just + # mess things up with partial states (due to lack of dependencies) real_models = [] for app_label in self.real_apps: app = global_apps.get_app_config(app_label) for model in app.get_models(): - real_models.append(ModelState.from_model(model)) + real_models.append(ModelState.from_model(model, exclude_rels=True)) # Populate the app registry with a stub for each application. app_labels = set(model_state.app_label for model_state in self.models.values()) self.apps = Apps([AppConfigStub(label) for label in sorted(self.real_apps + list(app_labels))]) @@ -155,13 +157,15 @@ class ModelState(object): ) @classmethod - def from_model(cls, model): + def from_model(cls, model, exclude_rels=False): """ Feed me a model, get a ModelState representing it out. """ # Deconstruct the fields fields = [] for field in model._meta.local_fields: + if getattr(field, "rel", None) and exclude_rels: + continue name, path, args, kwargs = field.deconstruct() field_class = import_string(path) try: @@ -173,17 +177,18 @@ class ModelState(object): model._meta.object_name, e, )) - for field in model._meta.local_many_to_many: - name, path, args, kwargs = field.deconstruct() - field_class = import_string(path) - try: - fields.append((name, field_class(*args, **kwargs))) - except TypeError as e: - raise TypeError("Couldn't reconstruct m2m field %s on %s: %s" % ( - name, - model._meta.object_name, - e, - )) + if not exclude_rels: + for field in model._meta.local_many_to_many: + name, path, args, kwargs = field.deconstruct() + field_class = import_string(path) + try: + fields.append((name, field_class(*args, **kwargs))) + except TypeError as e: + raise TypeError("Couldn't reconstruct m2m field %s on %s: %s" % ( + name, + model._meta.object_name, + e, + )) # Extract the options options = {} for name in DEFAULT_NAMES: diff --git a/tests/migrations/migrations_test_apps/migrated_app/__init__.py b/tests/migrations/migrations_test_apps/migrated_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/migrated_app/migrations/0001_initial.py b/tests/migrations/migrations_test_apps/migrated_app/migrations/0001_initial.py new file mode 100644 index 0000000000..581d536814 --- /dev/null +++ b/tests/migrations/migrations_test_apps/migrated_app/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + operations = [ + + migrations.CreateModel( + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(null=True)), + ("age", models.IntegerField(default=0)), + ("silly_field", models.BooleanField(default=False)), + ], + ), + + migrations.CreateModel( + "Tribble", + [ + ("id", models.AutoField(primary_key=True)), + ("fluffy", models.BooleanField(default=True)), + ], + ) + + ] diff --git a/tests/migrations/migrations_test_apps/migrated_app/migrations/__init__.py b/tests/migrations/migrations_test_apps/migrated_app/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/migrated_app/models.py b/tests/migrations/migrations_test_apps/migrated_app/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/migrated_unapplied_app/__init__.py b/tests/migrations/migrations_test_apps/migrated_unapplied_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/migrated_unapplied_app/migrations/0001_initial.py b/tests/migrations/migrations_test_apps/migrated_unapplied_app/migrations/0001_initial.py new file mode 100644 index 0000000000..4913065b10 --- /dev/null +++ b/tests/migrations/migrations_test_apps/migrated_unapplied_app/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + operations = [ + + migrations.CreateModel( + "OtherAuthor", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(null=True)), + ("age", models.IntegerField(default=0)), + ("silly_field", models.BooleanField(default=False)), + ], + ), + + ] diff --git a/tests/migrations/migrations_test_apps/migrated_unapplied_app/migrations/__init__.py b/tests/migrations/migrations_test_apps/migrated_unapplied_app/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/migrated_unapplied_app/models.py b/tests/migrations/migrations_test_apps/migrated_unapplied_app/models.py new file mode 100644 index 0000000000..efb71e637f --- /dev/null +++ b/tests/migrations/migrations_test_apps/migrated_unapplied_app/models.py @@ -0,0 +1,12 @@ +from django.db import models + + +class OtherAuthor(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=255) + slug = models.SlugField(null=True) + age = models.IntegerField(default=0) + silly_field = models.BooleanField(default=False) + + class Meta: + app_label = "migrated_unapplied_app" diff --git a/tests/migrations/migrations_test_apps/unmigrated_app/__init__.py b/tests/migrations/migrations_test_apps/unmigrated_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/migrations/migrations_test_apps/unmigrated_app/models.py b/tests/migrations/migrations_test_apps/unmigrated_app/models.py new file mode 100644 index 0000000000..c44e922d38 --- /dev/null +++ b/tests/migrations/migrations_test_apps/unmigrated_app/models.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from django.db import models + + +class SillyModel(models.Model): + silly_field = models.BooleanField(default=False) + silly_tribble = models.ForeignKey("migrations.Tribble") + is_trouble = models.BooleanField(default=True) diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index a44c415d7d..c223b5f489 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -102,6 +102,29 @@ class MigrateTests(MigrationTestBase): call_command("sqlmigrate", "migrations", "0001", stdout=stdout, backwards=True) self.assertIn("drop table", stdout.getvalue().lower()) + @override_system_checks([]) + @override_settings( + INSTALLED_APPS=[ + "migrations.migrations_test_apps.migrated_app", + "migrations.migrations_test_apps.migrated_unapplied_app", + "migrations.migrations_test_apps.unmigrated_app"]) + def test_regression_22823_unmigrated_fk_to_migrated_model(self): + """ + https://code.djangoproject.com/ticket/22823 + + Assuming you have 3 apps, `A`, `B`, and `C`, such that: + + * `A` has migrations + * `B` has a migration we want to apply + * `C` has no migrations, but has an FK to `A` + + When we try to migrate "B", an exception occurs because the + "B" was not included in the ProjectState that is used to detect + soft-applied migrations. + """ + stdout = six.StringIO() + call_command("migrate", "migrated_unapplied_app", stdout=stdout) + class MakeMigrationsTests(MigrationTestBase): """