Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

WIP Proxy model migrations #2645

Closed
wants to merge 11 commits into from
This page is out of date. Refresh to see the latest.
View
115 django/db/migrations/autodetector.py
@@ -53,20 +53,48 @@ def _detect_changes(self):
old_apps = self.from_state.render(ignore_swappable=True)
new_apps = self.to_state.render()
# Prepare lists of old/new model keys that we care about
- # (i.e. ignoring proxy ones and unmigrated ones)
+ # (i.e. unmigrated ones)
old_model_keys = []
for al, mn in self.from_state.models.keys():
model = old_apps.get_model(al, mn)
- if not model._meta.proxy and model._meta.managed and al not in self.from_state.real_apps:
+ if model._meta.managed and al not in self.from_state.real_apps:
old_model_keys.append((al, mn))
new_model_keys = []
for al, mn in self.to_state.models.keys():
model = new_apps.get_model(al, mn)
- if not model._meta.proxy and model._meta.managed and al not in self.to_state.real_apps:
+ if model._meta.managed and al not in self.to_state.real_apps:
new_model_keys.append((al, mn))
+ # TODO good idea treat proxy to non-proxy as new (for field handling)?
+ kept_models = set(old_model_keys).intersection(new_model_keys)
+ for app_label, model_name in kept_models:
+ old_model_state = self.from_state.models[app_label, model_name]
+ new_model_state = self.to_state.models[app_label, model_name]
+ if old_model_state.options.get('proxy') and not new_model_state.options.get('proxy'):
+ self.add_to_migration(
+ app_label,
+ operations.DeleteModel(
+ name=old_model_state.name,
+ orm_only=True,
+ )
+ )
+ old_model_keys.remove((app_label, model_name))
+ if not old_model_state.options.get('proxy') and new_model_state.options.get('proxy'):
+ self.add_to_migration(
+ app_label,
+ operations.CreateModel(
+ name=new_model_state.name,
+ fields=[],
+ options=new_model_state.options,
+ bases=new_model_state.bases,
+ orm_only=True,
+ )
+ )
+ new_model_keys.remove((app_label, model_name))
+
+
def _deep_deconstruct(obj, field=True):
"""
Recursive deconstruction for a field and its arguments.
@@ -112,11 +140,16 @@ def _rel_agnostic_fields_def(fields):
rem_model_fields_def = _rel_agnostic_fields_def(rem_model_state.fields)
if model_fields_def == rem_model_fields_def:
if self.questioner.ask_rename_model(rem_model_state, model_state):
+ if model_state.options.get('proxy'):
+ orm_only = True
+ else:
+ orm_only = False
self.add_to_migration(
app_label,
operations.RenameModel(
old_name=rem_model_state.name,
new_name=model_state.name,
+ orm_only=orm_only,
)
)
renamed_models[app_label, model_name] = rem_model_name
@@ -127,12 +160,16 @@ def _rel_agnostic_fields_def(fields):
# Adding models. Phase 1 is adding models with no outward relationships.
added_models = set(new_model_keys) - set(old_model_keys)
- pending_add = {}
+ pending_related_fields = {}
+ pending_proxy_base = {}
for app_label, model_name in added_models:
model_state = self.to_state.models[app_label, model_name]
- # Are there any relationships out from this model? if so, punt it to the next phase.
+ # Are there any relationships out from this model, or is it a proxy
+ # for another model? if so, punt it to the next phase.
related_fields = []
- for field in new_apps.get_model(app_label, model_name)._meta.local_fields:
+ options = new_apps.get_model(app_label, model_name)._meta
+ proxy_for_model = options.proxy_for_model
+ for field in options.local_fields:
if field.rel:
if field.rel.to:
related_fields.append((field.name, field.rel.to._meta.app_label, field.rel.to._meta.model_name))
@@ -144,7 +181,9 @@ def _rel_agnostic_fields_def(fields):
if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created:
related_fields.append((field.name, field.rel.through._meta.app_label, field.rel.through._meta.model_name))
if related_fields:
- pending_add[app_label, model_name] = related_fields
+ pending_related_fields[app_label, model_name] = related_fields
+ elif proxy_for_model is not None:
+ pending_proxy_base[app_label, model_name] = (proxy_for_model._meta.app_label, proxy_for_model._meta.model_name)
else:
self.add_to_migration(
app_label,
@@ -161,15 +200,20 @@ def _rel_agnostic_fields_def(fields):
pending_new_fks = []
pending_unique_together = []
added_phase_2 = set()
- while pending_add:
+ while pending_related_fields or pending_proxy_base:
# Is there one we can add that has all dependencies satisfied?
- satisfied = [
+ satisfied_related_fields = [
(m, rf)
- for m, rf in pending_add.items()
- if all((al, mn) not in pending_add for f, al, mn in rf)
+ for m, rf in pending_related_fields.items()
+ if all((al, mn) not in (pending_related_fields.keys() + pending_proxy_base.keys())
+ for f, al, mn in rf)
+ ]
+ satisfied_proxy_model_bases = [
+ m for m, parent in pending_proxy_base.items()
+ if parent not in (pending_related_fields.keys() + pending_proxy_base.keys())
]
- if satisfied:
- (app_label, model_name), related_fields = sorted(satisfied)[0]
+ if satisfied_related_fields:
+ (app_label, model_name), related_fields = sorted(satisfied_related_fields)[0]
model_state = self.to_state.models[app_label, model_name]
self.add_to_migration(
app_label,
@@ -184,9 +228,29 @@ def _rel_agnostic_fields_def(fields):
new=any((al, mn) in added_phase_2 for f, al, mn in related_fields),
)
added_phase_2.add((app_label, model_name))
+ elif satisfied_proxy_model_bases:
+ (app_label, model_name) = sorted(satisfied_proxy_model_bases)[0]
+ model_state = self.to_state.models[app_label, model_name]
+ other_app_label, _ = pending_proxy_base[(app_label, model_name)]
+ self.add_to_migration(
+ app_label,
+ operations.CreateModel(
+ name=model_state.name,
+ fields=[],
+ options=model_state.options,
+ bases=model_state.bases,
+ orm_only=True,
+ ),
+ # If it's already been added in phase 2 put it in a new
+ # migration for safety.
+ # TODO: do we need this for proxies?
+ new=any((al, mn) in added_phase_2 for f, al, mn in related_fields),
+ )
+ # TODO: do we need this for proxies?
+ added_phase_2.add((app_label, model_name))
# Ah well, we'll need to split one. Pick deterministically.
else:
- (app_label, model_name), related_fields = sorted(pending_add.items())[0]
+ (app_label, model_name), related_fields = sorted(pending_related_fields.items())[0]
model_state = self.to_state.models[app_label, model_name]
# Defer unique together constraints creation, see ticket #22275
unique_together_constraints = model_state.options.pop('unique_together', None)
@@ -194,7 +258,7 @@ def _rel_agnostic_fields_def(fields):
pending_unique_together.append((app_label, model_name,
unique_together_constraints))
# Work out the fields that need splitting out
- bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_add)
+ bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_related_fields)
# Create the model, without those
self.add_to_migration(
app_label,
@@ -208,14 +272,19 @@ def _rel_agnostic_fields_def(fields):
# Add the bad fields to be made in a phase 3
for field_name, (other_app_label, other_model_name) in bad_fields.items():
pending_new_fks.append((app_label, model_name, field_name, other_app_label))
- for field_name, other_app_label, other_model_name in related_fields:
- # If it depends on a swappable something, add a dynamic depend'cy
- swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting
- if swappable_setting is not None:
- self.add_swappable_dependency(app_label, swappable_setting)
- elif app_label != other_app_label:
+ if new_apps.get_model(app_label, model_name)._meta.proxy:
+ if app_label != other_app_label:
self.add_dependency(app_label, other_app_label)
- del pending_add[app_label, model_name]
+ pending_proxy_base.pop((app_label, model_name))
+ else:
+ for field_name, other_app_label, other_model_name in related_fields:
+ # If it depends on a swappable something, add a dynamic depend'cy
+ swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting
+ if swappable_setting is not None:
+ self.add_swappable_dependency(app_label, swappable_setting)
+ elif app_label != other_app_label:
+ self.add_dependency(app_label, other_app_label)
+ pending_related_fields.pop((app_label, model_name))
# Phase 3 is adding the final set of FKs as separate new migrations.
for app_label, model_name, field_name, other_app_label in pending_new_fks:
@@ -267,6 +336,7 @@ def _rel_agnostic_fields_def(fields):
unique_together=new_model_state.options.get("unique_together", set()),
)
))
+
# New fields
renamed_fields = {}
for app_label, model_name, field_name in new_fields - old_fields:
@@ -489,6 +559,7 @@ def suggest_name(cls, ops):
but we put some effort in to the fallback name to avoid VCS conflicts
if we can.
"""
+
if len(ops) == 1:
if isinstance(ops[0], operations.CreateModel):
return ops[0].name.lower()
View
28 django/db/migrations/operations/models.py
@@ -13,23 +13,29 @@ class CreateModel(Operation):
"""
serialization_expand_args = ['fields', 'options']
+ orm_only = False
- def __init__(self, name, fields, options=None, bases=None):
+ def __init__(self, name, fields, options=None, bases=None, orm_only=False):
self.name = name
self.fields = fields
self.options = options or {}
self.bases = bases or (models.Model,)
+ self.orm_only = orm_only
def state_forwards(self, app_label, state):
state.models[app_label, self.name.lower()] = ModelState(app_label, self.name, self.fields, self.options, self.bases)
def database_forwards(self, app_label, schema_editor, from_state, to_state):
+ if self.orm_only:
+ return
apps = to_state.render()
model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model):
schema_editor.create_model(model)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
+ if self.orm_only:
+ return
apps = from_state.render()
model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model):
@@ -64,25 +70,35 @@ def __eq__(self, other):
([(k, f.deconstruct()[1:]) for k, f in self.fields] == [(k, f.deconstruct()[1:]) for k, f in other.fields])
)
+ def __ne__(self, other):
+ return not (self == other)
+
class DeleteModel(Operation):
"""
Drops a model's table.
"""
- def __init__(self, name):
+ orm_only = False
+
+ def __init__(self, name, orm_only=False):
self.name = name
+ self.orm_only = orm_only
def state_forwards(self, app_label, state):
del state.models[app_label, self.name.lower()]
def database_forwards(self, app_label, schema_editor, from_state, to_state):
+ if self.orm_only:
+ return
apps = from_state.render()
model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model):
schema_editor.delete_model(model)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
+ if self.orm_only:
+ return
apps = to_state.render()
model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model):
@@ -101,10 +117,12 @@ class RenameModel(Operation):
"""
reversible = False
+ orm_only = False
- def __init__(self, old_name, new_name):
+ def __init__(self, old_name, new_name, orm_only=False):
self.old_name = old_name
self.new_name = new_name
+ self.orm_only = orm_only
def state_forwards(self, app_label, state):
state.models[app_label, self.new_name.lower()] = state.models[app_label, self.old_name.lower()]
@@ -112,6 +130,8 @@ def state_forwards(self, app_label, state):
del state.models[app_label, self.old_name.lower()]
def database_forwards(self, app_label, schema_editor, from_state, to_state):
+ if self.orm_only:
+ return
old_apps = from_state.render()
new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.old_name)
@@ -124,6 +144,8 @@ def database_forwards(self, app_label, schema_editor, from_state, to_state):
)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
+ if self.orm_only:
+ return
old_apps = from_state.render()
new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.new_name)
View
3  django/db/migrations/state.py
@@ -65,7 +65,8 @@ def render(self, include_real=None, ignore_swappable=False):
except InvalidBasesError:
new_unrendered_models.append(model)
if len(new_unrendered_models) == len(unrendered_models):
- raise InvalidBasesError("Cannot resolve bases for %r" % new_unrendered_models)
+ raise InvalidBasesError("Cannot resolve bases for %r" % [
+ s.name for s in new_unrendered_models])
unrendered_models = new_unrendered_models
# make sure apps has no dangling references
if self.apps._pending_lookups:
View
102 tests/migrations/test_autodetector.py
@@ -34,7 +34,9 @@ class AutodetectorTests(TestCase):
author_with_publisher = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("publisher", models.ForeignKey("testapp.Publisher"))])
author_with_custom_user = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("user", models.ForeignKey("thirdapp.CustomUser"))])
author_proxy = ModelState("testapp", "AuthorProxy", [], {"proxy": True}, ("testapp.author", ))
- author_proxy_notproxy = ModelState("testapp", "AuthorProxy", [], {}, ("testapp.author", ))
+ author_proxy_notproxy = ModelState("testapp", "AuthorProxy", [("author_ptr", models.OneToOneField("testapp.Author", primary_key=True))], {}, ("testapp.author", ))
+ author_proxy_renamed = ModelState("testapp", "WriterProxy", [], {"proxy": True}, ("testapp.author", ))
+ author_proxy_otherapp = ModelState("otherapp", "AuthorProxy", [], {"proxy": True}, ("testapp.author", ))
author_unmanaged = ModelState("testapp", "AuthorUnmanaged", [], {"managed": False}, ("testapp.author", ))
author_unmanaged_managed = ModelState("testapp", "AuthorUnmanaged", [], {}, ("testapp.author", ))
author_with_m2m = ModelState("testapp", "Author", [
@@ -57,6 +59,7 @@ class AutodetectorTests(TestCase):
book_unique_2 = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("title", "author")]})
book_unique_3 = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("newfield", models.IntegerField()), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("title", "newfield")]})
attribution = ModelState("otherapp", "Attribution", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("book", models.ForeignKey("otherapp.Book"))])
+ book_with_proxy_author = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.AuthorProxy")), ("title", models.CharField(max_length=200))])
edition = ModelState("thirdapp", "Edition", [("id", models.AutoField(primary_key=True)), ("book", models.ForeignKey("otherapp.Book"))])
custom_user = ModelState("thirdapp", "CustomUser", [("id", models.AutoField(primary_key=True)), ("username", models.CharField(max_length=255))])
knight = ModelState("eggs", "Knight", [("id", models.AutoField(primary_key=True))])
@@ -241,6 +244,26 @@ def test_rename_model(self):
self.assertEqual(action.name, "author")
self.assertEqual(action.field.rel.to.__name__, "Writer")
+ def test_rename_proxy_model(self):
+ "Tests autodetection of renamed proxy models"
+ # Make state
+ before = self.make_project_state([self.author_empty, self.author_proxy])
+ after = self.make_project_state([self.author_empty, self.author_proxy_renamed])
+ autodetector = MigrationAutodetector(before, after, MigrationQuestioner({"ask_rename_model": True}))
+ changes = autodetector._detect_changes()
+
+ # Right number of migrations for model rename?
+ self.assertEqual(len(changes['testapp']), 1)
+ # Right number of actions?
+ migration = changes['testapp'][0]
+ self.assertEqual(len(migration.operations), 1)
+ # Right action?
+ action = migration.operations[0]
+ self.assertEqual(action.__class__.__name__, "RenameModel")
+ self.assertEqual(action.old_name, "AuthorProxy")
+ self.assertEqual(action.new_name, "WriterProxy")
+ self.assertTrue(action.orm_only)
+
def test_rename_model_with_renamed_rel_field(self):
"""
Tests autodetection of renamed models while simultaneously renaming one
@@ -308,6 +331,21 @@ def test_fk_dependency(self):
self.assertEqual(migration2.dependencies, [("testapp", "auto_1")])
self.assertEqual(migration3.dependencies, [("otherapp", "auto_1")])
+ def test_proxy_dependency(self):
+ "Tests that being a proxy app automatically adds a dependency"
+ # Make state
+ before = self.make_project_state([self.author_empty])
+ after = self.make_project_state([self.author_empty, self.author_proxy_otherapp])
+ autodetector = MigrationAutodetector(before, after)
+ changes = autodetector._detect_changes()
+ # Right number of migrations?
+ self.assertEqual(len(changes['otherapp']), 1)
+ migration = changes['otherapp'][0]
+ action = migration.operations[0]
+ self.assertEqual(action.__class__.__name__, "CreateModel")
+ self.assertTrue(action.orm_only)
+ self.assertEqual(migration.dependencies, [("testapp", "__first__")])
+
def test_same_app_no_fk_dependency(self):
"""
Tests that a migration with a FK between two models of the same app
@@ -484,17 +522,45 @@ def test_add_field_and_unique_together(self):
self.assertEqual(action2.__class__.__name__, "AlterUniqueTogether")
self.assertEqual(action2.unique_together, set([("title", "newfield")]))
- def test_proxy_ignorance(self):
- "Tests that the autodetector correctly ignores proxy models"
- # First, we test adding a proxy model
+ def test_add_proxy(self):
+ "Tests adding a proxy model"
before = self.make_project_state([self.author_empty])
after = self.make_project_state([self.author_empty, self.author_proxy])
autodetector = MigrationAutodetector(before, after)
changes = autodetector._detect_changes()
# Right number of migrations?
- self.assertEqual(len(changes), 0)
+ self.assertEqual(len(changes['testapp']), 1)
+ # Right number of actions?
+ migration = changes['testapp'][0]
+ self.assertEqual(len(migration.operations), 1)
+ # Right actions?
+ action = migration.operations[0]
+ self.assertEqual(action.__class__.__name__, "CreateModel")
+ self.assertEqual(action.name, "AuthorProxy")
+ self.assertTrue(action.orm_only)
- # Now, we test turning a proxy model into a non-proxy model
+ def test_convert_to_proxy(self):
+ "Tests conversion of a non-proxy model to a proxy model"
+ before = self.make_project_state([self.author_empty, self.author_proxy_notproxy])
+ after = self.make_project_state([self.author_empty, self.author_proxy])
+ autodetector = MigrationAutodetector(before, after)
+ changes = autodetector._detect_changes()
+ # Right number of migrations?
+ self.assertEqual(len(changes['testapp']), 1)
+ # Right number of actions?
+ migration = changes['testapp'][0]
+ self.assertEqual(len(migration.operations), 2)
+ # Right actions?
+ action1, action2 = migration.operations
+ self.assertEqual(action1.__class__.__name__, "CreateModel")
+ self.assertEqual(action1.name, "AuthorProxy")
+ self.assertTrue(action1.orm_only)
+ self.assertEqual(action2.__class__.__name__, "DeleteModel")
+ self.assertEqual(action2.name, "AuthorProxy")
+ self.assertFalse(action2.orm_only)
+
+ def test_convert_from_proxy(self):
+ "Tests conversion of a non-proxy model to a proxy model"
before = self.make_project_state([self.author_empty, self.author_proxy])
after = self.make_project_state([self.author_empty, self.author_proxy_notproxy])
autodetector = MigrationAutodetector(before, after)
@@ -503,11 +569,25 @@ def test_proxy_ignorance(self):
self.assertEqual(len(changes['testapp']), 1)
# Right number of actions?
migration = changes['testapp'][0]
- self.assertEqual(len(migration.operations), 1)
- # Right action?
- action = migration.operations[0]
- self.assertEqual(action.__class__.__name__, "CreateModel")
- self.assertEqual(action.name, "AuthorProxy")
+ self.assertEqual(len(migration.operations), 2)
+ # Right actions?
+ action1, action2 = migration.operations
+ self.assertEqual(action1.__class__.__name__, "DeleteModel")
+ self.assertEqual(action1.name, "AuthorProxy")
+ self.assertTrue(action1.orm_only)
+ self.assertEqual(action2.__class__.__name__, "CreateModel")
+ self.assertEqual(action2.name, "AuthorProxy")
+ self.assertFalse(action2.orm_only)
+ self.assertEqual(len(action2.fields), 1)
+
+ def test_fk_to_proxy_dependencies(self):
+ "Tests foreign keys to proxy model"
+ before = self.make_project_state([self.author_empty, self.author_proxy])
+ after = self.make_project_state([self.author_empty, self.author_proxy, self.book_with_proxy_author])
+ autodetector = MigrationAutodetector(before, after)
+ changes = autodetector._detect_changes()
+ migration = changes['otherapp'][0]
+ self.assertEqual(migration.dependencies, [('testapp', '__first__')])
def test_unmanaged_ignorance(self):
"Tests that the autodetector correctly ignores managed models"
Something went wrong with that request. Please try again.