Commit 067b9668 authored by Andrew Godwin's avatar Andrew Godwin
Browse files

Fixed #22783: Make sure swappable models come first in creation

parent 2b79be2b
Loading
Loading
Loading
Loading
+23 −1
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ import datetime

from django.utils import six
from django.db import models
from django.conf import settings
from django.db.migrations import operations
from django.db.migrations.migration import Migration
from django.db.migrations.questioner import MigrationQuestioner
@@ -345,6 +346,27 @@ class MigrationAutodetector(object):
        operation._auto_deps = dependencies or []
        self.generated_operations.setdefault(app_label, []).append(operation)

    def swappable_first_key(self, item):
        """
        Sorting key function that places potential swappable models first in
        lists of created models (only real way to solve #22783)
        """
        try:
            model = self.new_apps.get_model(item[0], item[1])
            base_names = [base.__name__ for base in model.__bases__]
            string_version = "%s.%s" % (item[0], item[1])
            if (
                model._meta.swappable or
                "AbstractUser" in base_names or
                "AbstractBaseUser" in base_names or
                settings.AUTH_USER_MODEL.lower() == string_version.lower()
            ):
                return ("___" + item[0], "___" + item[1])
        except LookupError:
            pass
        return item


    def generate_renamed_models(self):
        """
        Finds any renamed models, and generates the operations for them,
@@ -388,7 +410,7 @@ class MigrationAutodetector(object):
        that might be deferred (e.g. unique_together, index_together)
        """
        added_models = set(self.new_model_keys) - set(self.old_model_keys)
        for app_label, model_name in sorted(added_models):
        for app_label, model_name in sorted(added_models, key=self.swappable_first_key):
            model_state = self.to_state.models[app_label, model_name]
            # Gather related fields
            related_fields = {}
+35 −1
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ from django.db.migrations.questioner import MigrationQuestioner
from django.db.migrations.state import ProjectState, ModelState
from django.db.migrations.graph import MigrationGraph
from django.db import models
from django.contrib.auth.models import AbstractBaseUser


class DeconstructableObject(object):
@@ -67,7 +68,9 @@ class AutodetectorTests(TestCase):
    book_unique_3 = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("newfield", models.IntegerField()), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("title", "newfield")]})
    attribution = ModelState("otherapp", "Attribution", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("book", models.ForeignKey("otherapp.Book"))])
    edition = ModelState("thirdapp", "Edition", [("id", models.AutoField(primary_key=True)), ("book", models.ForeignKey("otherapp.Book"))])
    custom_user = ModelState("thirdapp", "CustomUser", [("id", models.AutoField(primary_key=True)), ("username", models.CharField(max_length=255))])
    custom_user = ModelState("thirdapp", "CustomUser", [("id", models.AutoField(primary_key=True)), ("username", models.CharField(max_length=255))], bases=(AbstractBaseUser, ))
    custom_user_no_inherit = ModelState("thirdapp", "CustomUser", [("id", models.AutoField(primary_key=True)), ("username", models.CharField(max_length=255))])
    aardvark = ModelState("thirdapp", "Aardvark", [("id", models.AutoField(primary_key=True))])
    knight = ModelState("eggs", "Knight", [("id", models.AutoField(primary_key=True))])
    rabbit = ModelState("eggs", "Rabbit", [("id", models.AutoField(primary_key=True)), ("knight", models.ForeignKey("eggs.Knight")), ("parent", models.ForeignKey("eggs.Rabbit"))], {"unique_together": [("parent", "knight")]})

@@ -913,3 +916,34 @@ class AutodetectorTests(TestCase):
        self.assertOperationAttributes(changes, 'testapp', 0, 1, name="author", order_with_respect_to="book")
        # Make sure the _order field is not in the CreateModel fields
        self.assertNotIn("_order", [name for name, field in changes['testapp'][0].operations[0].fields])

    def test_swappable_first_inheritance(self):
        """
        Tests that swappable models get their CreateModel first.
        """
        # Make state
        before = self.make_project_state([])
        after = self.make_project_state([self.custom_user, self.aardvark])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        # Right number of migrations?
        self.assertNumberMigrations(changes, 'thirdapp', 1)
        self.assertOperationTypes(changes, 'thirdapp', 0, ["CreateModel", "CreateModel"])
        self.assertOperationAttributes(changes, 'thirdapp', 0, 0, name="CustomUser")
        self.assertOperationAttributes(changes, 'thirdapp', 0, 1, name="Aardvark")

    @override_settings(AUTH_USER_MODEL="thirdapp.CustomUser")
    def test_swappable_first_setting(self):
        """
        Tests that swappable models get their CreateModel first.
        """
        # Make state
        before = self.make_project_state([])
        after = self.make_project_state([self.custom_user_no_inherit, self.aardvark])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        # Right number of migrations?
        self.assertNumberMigrations(changes, 'thirdapp', 1)
        self.assertOperationTypes(changes, 'thirdapp', 0, ["CreateModel", "CreateModel"])
        self.assertOperationAttributes(changes, 'thirdapp', 0, 0, name="CustomUser")
        self.assertOperationAttributes(changes, 'thirdapp', 0, 1, name="Aardvark")