[1.7.x] Fixed #24037 -- Prevented data loss possibility when changing Meta.managed.

The migrations autodetector now issues AlterModelOptions operations for
Meta.managed changes instead of DeleteModel + CreateModel.

Thanks iambibhas for the report and Simon and Markus for review.

Backport of 061caa5b38 from master
This commit is contained in:
Tim Graham 2014-12-23 12:27:49 -05:00
parent ac098867c0
commit 51ea30a43b
4 changed files with 43 additions and 17 deletions

View File

@ -454,8 +454,9 @@ class MigrationAutodetector(object):
We also defer any model options that refer to collections of fields We also defer any model options that refer to collections of fields
that might be deferred (e.g. unique_together, index_together). that might be deferred (e.g. unique_together, index_together).
""" """
added_models = set(self.new_model_keys) - set(self.old_model_keys) old_keys = set(self.old_model_keys).union(self.old_unmanaged_keys)
added_unmanaged_models = set(self.new_unmanaged_keys) - set(self.old_unmanaged_keys) added_models = set(self.new_model_keys) - old_keys
added_unmanaged_models = set(self.new_unmanaged_keys) - old_keys
models = chain( models = chain(
sorted(added_models, key=self.swappable_first_key, reverse=True), sorted(added_models, key=self.swappable_first_key, reverse=True),
sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True) sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True)
@ -630,19 +631,14 @@ class MigrationAutodetector(object):
We also bring forward removal of any model options that refer to We also bring forward removal of any model options that refer to
collections of fields - the inverse of generate_created_models(). collections of fields - the inverse of generate_created_models().
""" """
deleted_models = set(self.old_model_keys) - set(self.new_model_keys) new_keys = set(self.new_model_keys).union(self.new_unmanaged_keys)
deleted_unmanaged_models = set(self.old_unmanaged_keys) - set(self.new_unmanaged_keys) deleted_models = set(self.old_model_keys) - new_keys
deleted_unmanaged_models = set(self.old_unmanaged_keys) - new_keys
models = chain(sorted(deleted_models), sorted(deleted_unmanaged_models)) models = chain(sorted(deleted_models), sorted(deleted_unmanaged_models))
for app_label, model_name in models: for app_label, model_name in models:
model_state = self.from_state.models[app_label, model_name] model_state = self.from_state.models[app_label, model_name]
model = self.old_apps.get_model(app_label, model_name) model = self.old_apps.get_model(app_label, model_name)
if not model._meta.managed: if not model._meta.managed:
self.add_operation(
app_label,
operations.DeleteModel(
name=model_state.name,
),
)
# Skip here, no need to handle fields for unmanaged models # Skip here, no need to handle fields for unmanaged models
continue continue
@ -952,7 +948,18 @@ class MigrationAutodetector(object):
makes an operation to represent them in state changes (in case Python makes an operation to represent them in state changes (in case Python
code in migrations needs them) code in migrations needs them)
""" """
models_to_check = self.kept_model_keys.union(self.kept_proxy_keys).union(self.kept_unmanaged_keys) models_to_check = self.kept_model_keys.union(
self.kept_proxy_keys
).union(
self.kept_unmanaged_keys
).union(
# unmanaged converted to managed
set(self.old_unmanaged_keys).intersection(self.new_model_keys)
).union(
# managed converted to unmanaged
set(self.old_model_keys).intersection(self.new_unmanaged_keys)
)
for app_label, model_name in sorted(models_to_check): for app_label, model_name in sorted(models_to_check):
old_model_name = self.renamed_models.get((app_label, model_name), model_name) 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] old_model_state = self.from_state.models[app_label, old_model_name]

View File

@ -356,6 +356,7 @@ class AlterModelOptions(Operation):
# Model options we want to compare and preserve in an AlterModelOptions op # Model options we want to compare and preserve in an AlterModelOptions op
ALTER_OPTION_KEYS = [ ALTER_OPTION_KEYS = [
"get_latest_by", "get_latest_by",
"managed",
"ordering", "ordering",
"permissions", "permissions",
"default_permissions", "default_permissions",

View File

@ -162,3 +162,8 @@ Bugfixes
* Added ``datetime.time`` support to migrations questioner (:ticket:`23998`). * Added ``datetime.time`` support to migrations questioner (:ticket:`23998`).
* Fixed admindocs crash on apps installed as eggs (:ticket:`23525`). * Fixed admindocs crash on apps installed as eggs (:ticket:`23525`).
* Changed migrations autodetector to generate an ``AlterModelOptions`` operation
instead of ``DeleteModel`` and ``CreateModel`` operations when changing
``Meta.managed``. This prevents data loss when changing ``managed`` from
``False`` to ``True`` and vice versa (:ticket:`24037`).

View File

@ -1021,7 +1021,7 @@ class AutodetectorTests(TestCase):
# The field name the FK on the book model points to # The field name the FK on the book model points to
self.assertEqual(changes['otherapp'][0].operations[0].fields[2][1].rel.field_name, 'pk_field') self.assertEqual(changes['otherapp'][0].operations[0].fields[2][1].rel.field_name, 'pk_field')
def test_unmanaged(self): def test_unmanaged_create(self):
"""Tests that the autodetector correctly deals with managed models.""" """Tests that the autodetector correctly deals with managed models."""
# First, we test adding an unmanaged model # First, we test adding an unmanaged model
before = self.make_project_state([self.author_empty]) before = self.make_project_state([self.author_empty])
@ -1031,9 +1031,10 @@ class AutodetectorTests(TestCase):
# Right number/type of migrations? # Right number/type of migrations?
self.assertNumberMigrations(changes, 'testapp', 1) self.assertNumberMigrations(changes, 'testapp', 1)
self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"]) self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"])
self.assertOperationAttributes(changes, 'testapp', 0, 0, name="AuthorUnmanaged") self.assertOperationAttributes(changes, 'testapp', 0, 0,
self.assertEqual(changes['testapp'][0].operations[0].options['managed'], False) name="AuthorUnmanaged", options={"managed": False})
def test_unmanaged_to_managed(self):
# Now, we test turning an unmanaged model into a managed model # Now, we test turning an unmanaged model into a managed model
before = self.make_project_state([self.author_empty, self.author_unmanaged]) before = self.make_project_state([self.author_empty, self.author_unmanaged])
after = self.make_project_state([self.author_empty, self.author_unmanaged_managed]) after = self.make_project_state([self.author_empty, self.author_unmanaged_managed])
@ -1041,9 +1042,21 @@ class AutodetectorTests(TestCase):
changes = autodetector._detect_changes() changes = autodetector._detect_changes()
# Right number/type of migrations? # Right number/type of migrations?
self.assertNumberMigrations(changes, 'testapp', 1) self.assertNumberMigrations(changes, 'testapp', 1)
self.assertOperationTypes(changes, 'testapp', 0, ["DeleteModel", "CreateModel"]) self.assertOperationTypes(changes, 'testapp', 0, ["AlterModelOptions"])
self.assertOperationAttributes(changes, 'testapp', 0, 0, name="AuthorUnmanaged") self.assertOperationAttributes(changes, 'testapp', 0, 0,
self.assertOperationAttributes(changes, 'testapp', 0, 1, name="AuthorUnmanaged") name="authorunmanaged", options={})
def test_managed_to_unmanaged(self):
# Now, we turn managed to unmanaged.
before = self.make_project_state([self.author_empty, self.author_unmanaged_managed])
after = self.make_project_state([self.author_empty, self.author_unmanaged])
autodetector = MigrationAutodetector(before, after)
changes = autodetector._detect_changes()
# Right number/type of migrations?
self.assertNumberMigrations(changes, 'testapp', 1)
self.assertOperationTypes(changes, "testapp", 0, ["AlterModelOptions"])
self.assertOperationAttributes(changes, "testapp", 0, 0,
name="authorunmanaged", options={"managed": False})
def test_unmanaged_custom_pk(self): def test_unmanaged_custom_pk(self):
""" """