[1.7.x] Fixed #22823 (and partly #22563) - FKs from unmigrated apps breaking state.

Thanks to bendavis78 for the test and diagnostic work.
This commit is contained in:
Andrew Godwin 2014-06-12 10:21:26 -07:00
parent 84714dfed7
commit 961c9d6c6b
12 changed files with 114 additions and 13 deletions

View File

@ -44,11 +44,13 @@ class ProjectState(object):
# Any apps in self.real_apps should have all their models included # 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 # in the render. We don't use the original model instances as there
# are some variables that refer to the Apps object. # 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 = [] real_models = []
for app_label in self.real_apps: for app_label in self.real_apps:
app = global_apps.get_app_config(app_label) app = global_apps.get_app_config(app_label)
for model in app.get_models(): 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. # Populate the app registry with a stub for each application.
app_labels = set(model_state.app_label for model_state in self.models.values()) 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))]) self.apps = Apps([AppConfigStub(label) for label in sorted(self.real_apps + list(app_labels))])
@ -155,13 +157,15 @@ class ModelState(object):
) )
@classmethod @classmethod
def from_model(cls, model): def from_model(cls, model, exclude_rels=False):
""" """
Feed me a model, get a ModelState representing it out. Feed me a model, get a ModelState representing it out.
""" """
# Deconstruct the fields # Deconstruct the fields
fields = [] fields = []
for field in model._meta.local_fields: for field in model._meta.local_fields:
if getattr(field, "rel", None) and exclude_rels:
continue
name, path, args, kwargs = field.deconstruct() name, path, args, kwargs = field.deconstruct()
field_class = import_string(path) field_class = import_string(path)
try: try:
@ -173,6 +177,7 @@ class ModelState(object):
model._meta.object_name, model._meta.object_name,
e, e,
)) ))
if not exclude_rels:
for field in model._meta.local_many_to_many: for field in model._meta.local_many_to_many:
name, path, args, kwargs = field.deconstruct() name, path, args, kwargs = field.deconstruct()
field_class = import_string(path) field_class = import_string(path)

View File

@ -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)),
],
)
]

View File

@ -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)),
],
),
]

View File

@ -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"

View File

@ -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)

View File

@ -102,6 +102,29 @@ class MigrateTests(MigrationTestBase):
call_command("sqlmigrate", "migrations", "0001", stdout=stdout, backwards=True) call_command("sqlmigrate", "migrations", "0001", stdout=stdout, backwards=True)
self.assertIn("drop table", stdout.getvalue().lower()) 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): class MakeMigrationsTests(MigrationTestBase):
""" """