mirror of
https://github.com/django/django.git
synced 2025-10-26 07:06:08 +00:00
[1.7.x] Fixed #22397 -- Issues removing M2M field with explicit through model
Changed the migration autodetector to remove models last so that FK
and M2M fields will not be left as dangling references. Added a check
in the migration state renderer to error out in the presence of
dangling references instead of leaving them as strings. Fixed a bug
in the sqlite backend to handle the deletion of M2M fields with
"through" models properly (i.e., do nothing successfully).
Thanks to melinath for report, loic for tests and andrewgodwin and
charettes for assistance with architecture.
Backport of 956bd64424 from master
This commit is contained in:
committed by
Simon Charette
parent
b06e45bbf6
commit
bc5d568e1e
@@ -33,11 +33,15 @@ class AutodetectorTests(TestCase):
|
||||
other_stable = ModelState("otherapp", "Stable", [("id", models.AutoField(primary_key=True))])
|
||||
third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))])
|
||||
book = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))])
|
||||
book_with_no_author = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("title", models.CharField(max_length=200))])
|
||||
book_with_author_renamed = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Writer")), ("title", models.CharField(max_length=200))])
|
||||
book_with_field_and_author_renamed = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("writer", models.ForeignKey("testapp.Writer")), ("title", models.CharField(max_length=200))])
|
||||
book_with_multiple_authors = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("authors", models.ManyToManyField("testapp.Author")), ("title", models.CharField(max_length=200))])
|
||||
book_with_multiple_authors_through_attribution = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("authors", models.ManyToManyField("testapp.Author", through="otherapp.Attribution")), ("title", models.CharField(max_length=200))])
|
||||
book_unique = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("author", "title")]})
|
||||
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"))])
|
||||
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))])
|
||||
@@ -552,7 +556,32 @@ class AutodetectorTests(TestCase):
|
||||
"""
|
||||
# Make state
|
||||
before = self.make_project_state([self.author_with_publisher_string])
|
||||
after = self.make_project_state([self.author_with_publisher])
|
||||
after = self.make_project_state([self.author_with_publisher, self.publisher])
|
||||
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), 3)
|
||||
# Right actions?
|
||||
action = migration.operations[0]
|
||||
self.assertEqual(action.__class__.__name__, "CreateModel")
|
||||
self.assertEqual(action.name, "Publisher")
|
||||
action = migration.operations[1]
|
||||
self.assertEqual(action.__class__.__name__, "AddField")
|
||||
self.assertEqual(action.name, "publisher")
|
||||
action = migration.operations[2]
|
||||
self.assertEqual(action.__class__.__name__, "RemoveField")
|
||||
self.assertEqual(action.name, "publisher_name")
|
||||
|
||||
def test_foreign_key_removed_before_target_model(self):
|
||||
"""
|
||||
Removing an FK and the model it targets in the same change must remove
|
||||
the FK field before the model to maintain consistency.
|
||||
"""
|
||||
before = self.make_project_state([self.author_with_publisher, self.publisher])
|
||||
after = self.make_project_state([self.author_name]) # removes both the model and FK
|
||||
autodetector = MigrationAutodetector(before, after)
|
||||
changes = autodetector._detect_changes()
|
||||
# Right number of migrations?
|
||||
@@ -560,10 +589,32 @@ class AutodetectorTests(TestCase):
|
||||
# Right number of actions?
|
||||
migration = changes['testapp'][0]
|
||||
self.assertEqual(len(migration.operations), 2)
|
||||
# Right actions?
|
||||
# Right actions in right order?
|
||||
action = migration.operations[0]
|
||||
self.assertEqual(action.__class__.__name__, "AddField")
|
||||
self.assertEqual(action.__class__.__name__, "RemoveField")
|
||||
self.assertEqual(action.name, "publisher")
|
||||
action = migration.operations[1]
|
||||
self.assertEqual(action.__class__.__name__, "DeleteModel")
|
||||
self.assertEqual(action.name, "Publisher")
|
||||
|
||||
def test_many_to_many_removed_before_through_model(self):
|
||||
"""
|
||||
Removing a ManyToManyField and the "through" model in the same change must remove
|
||||
the field before the model to maintain consistency.
|
||||
"""
|
||||
before = self.make_project_state([self.book_with_multiple_authors_through_attribution, self.author_name, self.attribution])
|
||||
after = self.make_project_state([self.book_with_no_author, self.author_name]) # removes both the through model and ManyToMany
|
||||
autodetector = MigrationAutodetector(before, after)
|
||||
changes = autodetector._detect_changes()
|
||||
# Right number of migrations?
|
||||
self.assertEqual(len(changes['otherapp']), 1)
|
||||
# Right number of actions?
|
||||
migration = changes['otherapp'][0]
|
||||
self.assertEqual(len(migration.operations), 2)
|
||||
# Right actions in right order?
|
||||
action = migration.operations[0]
|
||||
self.assertEqual(action.__class__.__name__, "RemoveField")
|
||||
self.assertEqual(action.name, "publisher_name")
|
||||
self.assertEqual(action.name, "authors")
|
||||
action = migration.operations[1]
|
||||
self.assertEqual(action.__class__.__name__, "DeleteModel")
|
||||
self.assertEqual(action.name, "Attribution")
|
||||
|
||||
Reference in New Issue
Block a user