mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Persist non-schema-relevant Meta changes in migrations
This commit is contained in:
		| @@ -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, | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| 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', | ||||
| ] | ||||
|   | ||||
| @@ -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, ) | ||||
|   | ||||
| @@ -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"]) | ||||
|   | ||||
| @@ -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): | ||||
|         """ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user