summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2014-06-15 12:34:02 -0700
committerAndrew Godwin <andrew@aeracode.org>2014-06-15 12:34:34 -0700
commit3ef87f664bb8b2a46546de755bfc4e158c90032f (patch)
tree6069996b70e34c31360edaaa7f427f6069240e3b
parent13aa079941dcfd953ba03a83f55c8a14399d15c2 (diff)
[1.7.x] Persist non-schema-relevant Meta changes in migrations
-rw-r--r--django/db/migrations/autodetector.py34
-rw-r--r--django/db/migrations/operations/__init__.py4
-rw-r--r--django/db/migrations/operations/models.py29
-rw-r--r--tests/migrations/test_autodetector.py15
-rw-r--r--tests/migrations/test_operations.py13
5 files changed, 93 insertions, 2 deletions
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index 1be3df96f7..a9daa33124 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -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,
diff --git a/django/db/migrations/operations/__init__.py b/django/db/migrations/operations/__init__.py
index 440b930526..b8a0ec7569 100644
--- a/django/db/migrations/operations/__init__.py
+++ b/django/db/migrations/operations/__init__.py
@@ -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',
]
diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py
index abd7ecd8f1..04dd957019 100644
--- a/django/db/migrations/operations/models.py
+++ b/django/db/migrations/operations/models.py
@@ -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, )
diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py
index 48c76d1bca..45687418d2 100644
--- a/tests/migrations/test_autodetector.py
+++ b/tests/migrations/test_autodetector.py
@@ -42,6 +42,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))])
@@ -787,3 +788,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"])
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 472d7b003e..fccc6b8cd7 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -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):
"""