[1.7.x] Persist non-schema-relevant Meta changes in migrations
This commit is contained in:
parent
13aa079941
commit
3ef87f664b
|
@ -23,6 +23,17 @@ class MigrationAutodetector(object):
|
||||||
if it wishes, with the caveat that it may not always be possible.
|
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):
|
def __init__(self, from_state, to_state, questioner=None):
|
||||||
self.from_state = from_state
|
self.from_state = from_state
|
||||||
self.to_state = to_state
|
self.to_state = to_state
|
||||||
|
@ -144,6 +155,7 @@ class MigrationAutodetector(object):
|
||||||
# Generate non-rename model operations
|
# Generate non-rename model operations
|
||||||
self.generate_created_models()
|
self.generate_created_models()
|
||||||
self.generate_deleted_models()
|
self.generate_deleted_models()
|
||||||
|
self.generate_altered_options()
|
||||||
|
|
||||||
# Generate field operations
|
# Generate field operations
|
||||||
self.generate_added_fields()
|
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):
|
def arrange_for_graph(self, changes, graph):
|
||||||
"""
|
"""
|
||||||
Takes in a result from changes() and a MigrationGraph,
|
Takes in a result from changes() and a MigrationGraph,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from .models import (CreateModel, DeleteModel, AlterModelTable,
|
from .models import (CreateModel, DeleteModel, AlterModelTable,
|
||||||
AlterUniqueTogether, AlterIndexTogether, RenameModel)
|
AlterUniqueTogether, AlterIndexTogether, RenameModel, AlterModelOptions)
|
||||||
from .fields import AddField, RemoveField, AlterField, RenameField
|
from .fields import AddField, RemoveField, AlterField, RenameField
|
||||||
from .special import SeparateDatabaseAndState, RunSQL, RunPython
|
from .special import SeparateDatabaseAndState, RunSQL, RunPython
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
|
'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
|
||||||
'RenameModel', 'AlterIndexTogether',
|
'RenameModel', 'AlterIndexTogether', 'AlterModelOptions',
|
||||||
'AddField', 'RemoveField', 'AlterField', 'RenameField',
|
'AddField', 'RemoveField', 'AlterField', 'RenameField',
|
||||||
'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
|
'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
|
||||||
]
|
]
|
||||||
|
|
|
@ -283,3 +283,32 @@ class AlterIndexTogether(Operation):
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
return "Alter index_together for %s (%s constraints)" % (self.name, len(self.index_together))
|
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, )
|
||||||
|
|
|
@ -42,6 +42,7 @@ class AutodetectorTests(TestCase):
|
||||||
("publishers", models.ManyToManyField("testapp.Publisher")),
|
("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_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"))])
|
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 = 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))])
|
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)
|
self.assertNumberMigrations(changes, "testapp", 1)
|
||||||
# Right actions in right order?
|
# Right actions in right order?
|
||||||
self.assertOperationTypes(changes, "testapp", 0, ["RemoveField", "RemoveField", "DeleteModel", "DeleteModel"])
|
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)
|
operation.database_backwards("test_alinto", editor, new_state, project_state)
|
||||||
self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])
|
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")
|
@unittest.skipIf(sqlparse is None and connection.features.requires_sqlparse_for_splitting, "Missing sqlparse")
|
||||||
def test_run_sql(self):
|
def test_run_sql(self):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue