Commit a58f49d1 authored by Andrew Godwin's avatar Andrew Godwin
Browse files

Persist non-schema-relevant Meta changes in migrations

parent f717ef08
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
@@ -23,6 +23,17 @@ class MigrationAutodetector(object):
    if it wishes, with the caveat that it may not always be possible.
    """

    # Model options we want to compare and preserve in an AlterModelOptions op
    ALTER_OPTION_KEYS = [
        "get_latest_by",
        "ordering",
        "permissions",
        "default_permissions",
        "select_on_save",
        "verbose_name",
        "verbose_name_plural",
    ]

    def __init__(self, from_state, to_state, questioner=None):
        self.from_state = from_state
        self.to_state = to_state
@@ -144,6 +155,7 @@ class MigrationAutodetector(object):
        # Generate non-rename model operations
        self.generate_created_models()
        self.generate_deleted_models()
        self.generate_altered_options()

        # Generate field operations
        self.generate_added_fields()
@@ -646,6 +658,28 @@ class MigrationAutodetector(object):
                    )
                )

    def generate_altered_options(self):
        for app_label, model_name in sorted(self.kept_model_keys):
            old_model_name = self.renamed_models.get((app_label, model_name), model_name)
            old_model_state = self.from_state.models[app_label, old_model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            old_options = dict(
                option for option in old_model_state.options.items()
                if option[0] in self.ALTER_OPTION_KEYS
            )
            new_options = dict(
                option for option in new_model_state.options.items()
                if option[0] in self.ALTER_OPTION_KEYS
            )
            if old_options != new_options:
                self.add_operation(
                    app_label,
                    operations.AlterModelOptions(
                        name=model_name,
                        options=new_options,
                    )
                )

    def arrange_for_graph(self, changes, graph):
        """
        Takes in a result from changes() and a MigrationGraph,
+2 −2
Original line number Diff line number Diff line
from .models import (CreateModel, DeleteModel, AlterModelTable,
    AlterUniqueTogether, AlterIndexTogether, RenameModel)
    AlterUniqueTogether, AlterIndexTogether, RenameModel, AlterModelOptions)
from .fields import AddField, RemoveField, AlterField, RenameField
from .special import SeparateDatabaseAndState, RunSQL, RunPython

__all__ = [
    'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
    'RenameModel', 'AlterIndexTogether',
    'RenameModel', 'AlterIndexTogether', 'AlterModelOptions',
    'AddField', 'RemoveField', 'AlterField', 'RenameField',
    'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
]
+29 −0
Original line number Diff line number Diff line
@@ -283,3 +283,32 @@ class AlterIndexTogether(Operation):

    def describe(self):
        return "Alter index_together for %s (%s constraints)" % (self.name, len(self.index_together))


class AlterModelOptions(Operation):
    """
    Sets new model options that don't directly affect the database schema
    (like verbose_name, permissions, ordering). Python code in migrations
    may still need them.
    """

    def __init__(self, name, options):
        self.name = name
        self.options = options

    def state_forwards(self, app_label, state):
        model_state = state.models[app_label, self.name.lower()]
        model_state.options = dict(model_state.options)
        model_state.options.update(self.options)

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        pass

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        pass

    def references_model(self, name, app_label=None):
        return name.lower() == self.name.lower()

    def describe(self):
        return "Change Meta options on %s" % (self.name, )
+15 −0
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ class AutodetectorTests(TestCase):
        ("publishers", models.ManyToManyField("testapp.Publisher")),
    ])
    author_with_m2m_through = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("publishers", models.ManyToManyField("testapp.Publisher", through="testapp.Contract"))])
    author_with_options = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True))], {"verbose_name": "Authi", "permissions": [('can_hire', 'Can hire')]})
    contract = ModelState("testapp", "Contract", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("publisher", models.ForeignKey("testapp.Publisher"))])
    publisher = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=100))])
    publisher_with_author = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("name", models.CharField(max_length=100))])
@@ -799,3 +800,17 @@ class AutodetectorTests(TestCase):
        self.assertNumberMigrations(changes, "testapp", 1)
        # Right actions in right order?
        self.assertOperationTypes(changes, "testapp", 0, ["RemoveField", "RemoveField", "DeleteModel", "DeleteModel"])

    def test_alter_model_options(self):
        """
        If two models with a ForeignKey from one to the other are removed at the same time,
        the autodetector should remove them in the correct order.
        """
        before = self.make_project_state([self.author_empty])
        after = self.make_project_state([self.author_with_options])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        # Right number of migrations?
        self.assertNumberMigrations(changes, "testapp", 1)
        # Right actions in right order?
        self.assertOperationTypes(changes, "testapp", 0, ["AlterModelOptions"])
+13 −0
Original line number Diff line number Diff line
@@ -790,6 +790,19 @@ class OperationTests(MigrationTestBase):
            operation.database_backwards("test_alinto", editor, new_state, project_state)
        self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])

    def test_alter_model_options(self):
        """
        Tests the AlterModelOptions operation.
        """
        project_state = self.set_up_test_model("test_almoop")
        # Test the state alteration (no DB alteration to test)
        operation = migrations.AlterModelOptions("Pony", {"permissions": [("can_groom", "Can groom")]})
        new_state = project_state.clone()
        operation.state_forwards("test_almoop", new_state)
        self.assertEqual(len(project_state.models["test_almoop", "pony"].options.get("permissions", [])), 0)
        self.assertEqual(len(new_state.models["test_almoop", "pony"].options.get("permissions", [])), 1)
        self.assertEqual(new_state.models["test_almoop", "pony"].options["permissions"][0][0], "can_groom")

    @unittest.skipIf(sqlparse is None and connection.features.requires_sqlparse_for_splitting, "Missing sqlparse")
    def test_run_sql(self):
        """