Loading django/db/migrations/autodetector.py +34 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() Loading Loading @@ -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, Loading django/db/migrations/operations/__init__.py +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', ] django/db/migrations/operations/models.py +29 −0 Original line number Diff line number Diff line Loading @@ -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, ) tests/migrations/test_autodetector.py +15 −0 Original line number Diff line number Diff line Loading @@ -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))]) Loading Loading @@ -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"]) tests/migrations/test_operations.py +13 −0 Original line number Diff line number Diff line Loading @@ -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): """ Loading Loading
django/db/migrations/autodetector.py +34 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() Loading Loading @@ -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, Loading
django/db/migrations/operations/__init__.py +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', ]
django/db/migrations/operations/models.py +29 −0 Original line number Diff line number Diff line Loading @@ -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, )
tests/migrations/test_autodetector.py +15 −0 Original line number Diff line number Diff line Loading @@ -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))]) Loading Loading @@ -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"])
tests/migrations/test_operations.py +13 −0 Original line number Diff line number Diff line Loading @@ -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): """ Loading