Fixed #22853: Swapped models are now ignored for migration operations.

This commit is contained in:
Andrew Godwin 2014-06-17 17:45:38 -07:00
parent 91f1b6dcdc
commit 8d2ac948a9
6 changed files with 147 additions and 26 deletions

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import router
class Operation(object): class Operation(object):
@ -97,6 +98,17 @@ class Operation(object):
""" """
return self.references_model(model_name, app_label) return self.references_model(model_name, app_label)
def allowed_to_migrate(self, connection_alias, model):
"""
Returns if we're allowed to migrate the model. Checks the router,
if it's a proxy, and if it's swapped out.
"""
return (
router.allow_migrate(connection_alias, model) and
not model._meta.proxy and
not model._meta.swapped
)
def __repr__(self): def __repr__(self):
return "<%s %s%s>" % ( return "<%s %s%s>" % (
self.__class__.__name__, self.__class__.__name__,

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import router
from django.db.models.fields import NOT_PROVIDED from django.db.models.fields import NOT_PROVIDED
from django.utils import six from django.utils import six
from .base import Operation from .base import Operation
@ -29,7 +28,7 @@ class AddField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.model_name) from_model = from_state.render().get_model(app_label, self.model_name)
to_model = to_state.render().get_model(app_label, self.model_name) to_model = to_state.render().get_model(app_label, self.model_name)
if router.allow_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
field = to_model._meta.get_field_by_name(self.name)[0] field = to_model._meta.get_field_by_name(self.name)[0]
if not self.preserve_default: if not self.preserve_default:
field.default = self.field.default field.default = self.field.default
@ -42,7 +41,7 @@ class AddField(Operation):
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.model_name) from_model = from_state.render().get_model(app_label, self.model_name)
if router.allow_migrate(schema_editor.connection.alias, from_model): if self.allowed_to_migrate(schema_editor.connection.alias, from_model):
schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0]) schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0])
def describe(self): def describe(self):
@ -81,13 +80,13 @@ class RemoveField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.model_name) from_model = from_state.render().get_model(app_label, self.model_name)
if router.allow_migrate(schema_editor.connection.alias, from_model): if self.allowed_to_migrate(schema_editor.connection.alias, from_model):
schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0]) schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0])
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.model_name) from_model = from_state.render().get_model(app_label, self.model_name)
to_model = to_state.render().get_model(app_label, self.model_name) to_model = to_state.render().get_model(app_label, self.model_name)
if router.allow_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
schema_editor.add_field(from_model, to_model._meta.get_field_by_name(self.name)[0]) schema_editor.add_field(from_model, to_model._meta.get_field_by_name(self.name)[0])
def describe(self): def describe(self):
@ -118,7 +117,7 @@ class AlterField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.model_name) from_model = from_state.render().get_model(app_label, self.model_name)
to_model = to_state.render().get_model(app_label, self.model_name) to_model = to_state.render().get_model(app_label, self.model_name)
if router.allow_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
from_field = from_model._meta.get_field_by_name(self.name)[0] from_field = from_model._meta.get_field_by_name(self.name)[0]
to_field = to_model._meta.get_field_by_name(self.name)[0] to_field = to_model._meta.get_field_by_name(self.name)[0]
# If the field is a relatedfield with an unresolved rel.to, just # If the field is a relatedfield with an unresolved rel.to, just
@ -170,7 +169,7 @@ class RenameField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.model_name) from_model = from_state.render().get_model(app_label, self.model_name)
to_model = to_state.render().get_model(app_label, self.model_name) to_model = to_state.render().get_model(app_label, self.model_name)
if router.allow_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
schema_editor.alter_field( schema_editor.alter_field(
from_model, from_model,
from_model._meta.get_field_by_name(self.old_name)[0], from_model._meta.get_field_by_name(self.old_name)[0],
@ -180,7 +179,7 @@ class RenameField(Operation):
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.model_name) from_model = from_state.render().get_model(app_label, self.model_name)
to_model = to_state.render().get_model(app_label, self.model_name) to_model = to_state.render().get_model(app_label, self.model_name)
if router.allow_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
schema_editor.alter_field( schema_editor.alter_field(
from_model, from_model,
from_model._meta.get_field_by_name(self.new_name)[0], from_model._meta.get_field_by_name(self.new_name)[0],

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models, router from django.db import models
from django.db.models.options import normalize_together from django.db.models.options import normalize_together
from django.db.migrations.state import ModelState from django.db.migrations.state import ModelState
from django.db.migrations.operations.base import Operation from django.db.migrations.operations.base import Operation
@ -32,13 +32,13 @@ class CreateModel(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
apps = to_state.render() apps = to_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model) and not model._meta.proxy: if self.allowed_to_migrate(schema_editor.connection.alias, model):
schema_editor.create_model(model) schema_editor.create_model(model)
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
apps = from_state.render() apps = from_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model) and not model._meta.proxy: if self.allowed_to_migrate(schema_editor.connection.alias, model):
schema_editor.delete_model(model) schema_editor.delete_model(model)
def describe(self): def describe(self):
@ -85,13 +85,13 @@ class DeleteModel(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
apps = from_state.render() apps = from_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model) and not model._meta.proxy: if self.allowed_to_migrate(schema_editor.connection.alias, model):
schema_editor.delete_model(model) schema_editor.delete_model(model)
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
apps = to_state.render() apps = to_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model) and not model._meta.proxy: if self.allowed_to_migrate(schema_editor.connection.alias, model):
schema_editor.create_model(model) schema_editor.create_model(model)
def references_model(self, name, app_label=None): def references_model(self, name, app_label=None):
@ -141,7 +141,7 @@ class RenameModel(Operation):
new_apps = to_state.render() new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.old_name) old_model = old_apps.get_model(app_label, self.old_name)
new_model = new_apps.get_model(app_label, self.new_name) new_model = new_apps.get_model(app_label, self.new_name)
if router.allow_migrate(schema_editor.connection.alias, new_model): if self.allowed_to_migrate(schema_editor.connection.alias, new_model):
# Move the main table # Move the main table
schema_editor.alter_db_table( schema_editor.alter_db_table(
new_model, new_model,
@ -194,7 +194,7 @@ class AlterModelTable(Operation):
new_apps = to_state.render() new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.name) old_model = old_apps.get_model(app_label, self.name)
new_model = new_apps.get_model(app_label, self.name) new_model = new_apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, new_model): if self.allowed_to_migrate(schema_editor.connection.alias, new_model):
schema_editor.alter_db_table( schema_editor.alter_db_table(
new_model, new_model,
old_model._meta.db_table, old_model._meta.db_table,
@ -231,7 +231,7 @@ class AlterUniqueTogether(Operation):
new_apps = to_state.render() new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.name) old_model = old_apps.get_model(app_label, self.name)
new_model = new_apps.get_model(app_label, self.name) new_model = new_apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, new_model): if self.allowed_to_migrate(schema_editor.connection.alias, new_model):
schema_editor.alter_unique_together( schema_editor.alter_unique_together(
new_model, new_model,
getattr(old_model._meta, "unique_together", set()), getattr(old_model._meta, "unique_together", set()),
@ -268,7 +268,7 @@ class AlterIndexTogether(Operation):
new_apps = to_state.render() new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.name) old_model = old_apps.get_model(app_label, self.name)
new_model = new_apps.get_model(app_label, self.name) new_model = new_apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, new_model): if self.allowed_to_migrate(schema_editor.connection.alias, new_model):
schema_editor.alter_index_together( schema_editor.alter_index_together(
new_model, new_model,
getattr(old_model._meta, "index_together", set()), getattr(old_model._meta, "index_together", set()),
@ -301,7 +301,7 @@ class AlterOrderWithRespectTo(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.render().get_model(app_label, self.name) from_model = from_state.render().get_model(app_label, self.name)
to_model = to_state.render().get_model(app_label, self.name) to_model = to_state.render().get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
# Remove a field if we need to # Remove a field if we need to
if from_model._meta.order_with_respect_to and not to_model._meta.order_with_respect_to: if from_model._meta.order_with_respect_to and not to_model._meta.order_with_respect_to:
schema_editor.remove_field(from_model, from_model._meta.get_field_by_name("_order")[0]) schema_editor.remove_field(from_model, from_model._meta.get_field_by_name("_order")[0])

View File

@ -2374,6 +2374,12 @@ Default: 'auth.User'
The model to use to represent a User. See :ref:`auth-custom-user`. The model to use to represent a User. See :ref:`auth-custom-user`.
.. warning::
You cannot change the AUTH_USER_MODEL setting during the lifetime of
a project (i.e. once you have made and migrated models that depend on it)
without serious effort. It is intended to be set at the project start.
See :ref:`auth-custom-user` for more details.
.. setting:: LOGIN_REDIRECT_URL .. setting:: LOGIN_REDIRECT_URL
LOGIN_REDIRECT_URL LOGIN_REDIRECT_URL

View File

@ -389,7 +389,8 @@ use as your User model.
Changing this setting after you have tables created is not supported Changing this setting after you have tables created is not supported
by :djadmin:`makemigrations` and will result in you having to manually by :djadmin:`makemigrations` and will result in you having to manually
write a set of migrations to fix your schema. fix your schema, port your data from the old user table, and possibly
manually reapply some migrations.
Referencing the User model Referencing the User model
-------------------------- --------------------------

View File

@ -8,6 +8,7 @@ except ImportError:
sqlparse = None sqlparse = None
from django import test from django import test
from django.test import override_settings
from django.db import connection, migrations, models, router from django.db import connection, migrations, models, router
from django.db.migrations.migration import Migration from django.db.migrations.migration import Migration
from django.db.migrations.state import ProjectState from django.db.migrations.state import ProjectState
@ -18,11 +19,9 @@ from django.db.utils import IntegrityError, DatabaseError
from .test_base import MigrationTestBase from .test_base import MigrationTestBase
class OperationTests(MigrationTestBase): class OperationTestBase(MigrationTestBase):
""" """
Tests running the operations and making sure they do what they say they do. Common functions to help test operations.
Each test looks at their state changing, and then their database operation -
both forwards and backwards.
""" """
def apply_operations(self, app_label, project_state, operations): def apply_operations(self, app_label, project_state, operations):
@ -37,6 +36,16 @@ class OperationTests(MigrationTestBase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
return migration.unapply(project_state, editor) return migration.unapply(project_state, editor)
def make_test_state(self, app_label, operation, **kwargs):
"""
Makes a test state using set_up_test_model and returns the
original state and the state after the migration is applied.
"""
project_state = self.set_up_test_model(app_label, **kwargs)
new_state = project_state.clone()
operation.state_forwards(app_label, new_state)
return project_state, new_state
def set_up_test_model(self, app_label, second_model=False, third_model=False, related_model=False, mti_model=False, proxy_model=False): def set_up_test_model(self, app_label, second_model=False, third_model=False, related_model=False, mti_model=False, proxy_model=False):
""" """
Creates a test model state and database table. Creates a test model state and database table.
@ -74,6 +83,9 @@ class OperationTests(MigrationTestBase):
("pink", models.IntegerField(default=3)), ("pink", models.IntegerField(default=3)),
("weight", models.FloatField()), ("weight", models.FloatField()),
], ],
options = {
"swappable": "TEST_SWAP_MODEL",
},
)] )]
if second_model: if second_model:
operations.append(migrations.CreateModel( operations.append(migrations.CreateModel(
@ -122,6 +134,15 @@ class OperationTests(MigrationTestBase):
return self.apply_operations(app_label, ProjectState(), operations) return self.apply_operations(app_label, ProjectState(), operations)
class OperationTests(OperationTestBase):
"""
Tests running the operations and making sure they do what they say they do.
Each test looks at their state changing, and then their database operation -
both forwards and backwards.
"""
def test_create_model(self): def test_create_model(self):
""" """
Tests the CreateModel operation. Tests the CreateModel operation.
@ -382,15 +403,13 @@ class OperationTests(MigrationTestBase):
""" """
Tests the AddField operation. Tests the AddField operation.
""" """
project_state = self.set_up_test_model("test_adfl")
# Test the state alteration # Test the state alteration
operation = migrations.AddField( operation = migrations.AddField(
"Pony", "Pony",
"height", "height",
models.FloatField(null=True, default=5), models.FloatField(null=True, default=5),
) )
new_state = project_state.clone() project_state, new_state = self.make_test_state("test_adfl", operation)
operation.state_forwards("test_adfl", new_state)
self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 4) self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 4)
field = [ field = [
f for n, f in new_state.models["test_adfl", "pony"].fields f for n, f in new_state.models["test_adfl", "pony"].fields
@ -1117,3 +1136,87 @@ class MultiDBOperationTests(MigrationTestBase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
operation.database_backwards("test_crmo", editor, new_state, project_state) operation.database_backwards("test_crmo", editor, new_state, project_state)
self.assertTableNotExists("test_crmo_pony") self.assertTableNotExists("test_crmo_pony")
class SwappableOperationTests(OperationTestBase):
"""
Tests that key operations ignore swappable models
(we don't want to replicate all of them here, as the functionality
is in a common base class anyway)
"""
available_apps = [
"migrations",
"django.contrib.auth",
]
@override_settings(TEST_SWAP_MODEL="migrations.SomeFakeModel")
def test_create_ignore_swapped(self):
"""
Tests that the CreateTable operation ignores swapped models.
"""
operation = migrations.CreateModel(
"Pony",
[
("id", models.AutoField(primary_key=True)),
("pink", models.IntegerField(default=1)),
],
options = {
"swappable": "TEST_SWAP_MODEL",
},
)
# Test the state alteration (it should still be there!)
project_state = ProjectState()
new_state = project_state.clone()
operation.state_forwards("test_crigsw", new_state)
self.assertEqual(new_state.models["test_crigsw", "pony"].name, "Pony")
self.assertEqual(len(new_state.models["test_crigsw", "pony"].fields), 2)
# Test the database alteration
self.assertTableNotExists("test_crigsw_pony")
with connection.schema_editor() as editor:
operation.database_forwards("test_crigsw", editor, project_state, new_state)
self.assertTableNotExists("test_crigsw_pony")
# And test reversal
with connection.schema_editor() as editor:
operation.database_backwards("test_crigsw", editor, new_state, project_state)
self.assertTableNotExists("test_crigsw_pony")
@override_settings(TEST_SWAP_MODEL="migrations.SomeFakeModel")
def test_delete_ignore_swapped(self):
"""
Tests the DeleteModel operation ignores swapped models.
"""
operation = migrations.DeleteModel("Pony")
project_state, new_state = self.make_test_state("test_dligsw", operation)
# Test the database alteration
self.assertTableNotExists("test_dligsw_pony")
with connection.schema_editor() as editor:
operation.database_forwards("test_dligsw", editor, project_state, new_state)
self.assertTableNotExists("test_dligsw_pony")
# And test reversal
with connection.schema_editor() as editor:
operation.database_backwards("test_dligsw", editor, new_state, project_state)
self.assertTableNotExists("test_dligsw_pony")
@override_settings(TEST_SWAP_MODEL="migrations.SomeFakeModel")
def test_add_field_ignore_swapped(self):
"""
Tests the AddField operation.
"""
# Test the state alteration
operation = migrations.AddField(
"Pony",
"height",
models.FloatField(null=True, default=5),
)
project_state, new_state = self.make_test_state("test_adfligsw", operation)
# Test the database alteration
self.assertTableNotExists("test_adfligsw_pont")
self.assertColumnNotExists("test_adfligsw_pony", "height")
with connection.schema_editor() as editor:
operation.database_forwards("test_adfligsw", editor, project_state, new_state)
self.assertColumnNotExists("test_adfligsw_pony", "height")
# And test reversal
with connection.schema_editor() as editor:
operation.database_backwards("test_adfligsw", editor, new_state, project_state)
self.assertColumnNotExists("test_adfligsw_pony", "height")