Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion django/db/migrations/autodetector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,10 @@ def generate_altered_fields(self):
if old_field_dec != new_field_dec and old_field_name == field_name:
both_m2m = old_field.many_to_many and new_field.many_to_many
neither_m2m = not old_field.many_to_many and not new_field.many_to_many
if both_m2m or neither_m2m:
target_changed = (
both_m2m and new_field_dec[2]["to"] != old_field_dec[2]["to"]
)
if (both_m2m or neither_m2m) and not target_changed:
# Either both fields are m2m or neither is
preserve_default = True
if (
Expand Down
2 changes: 1 addition & 1 deletion django/forms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def add_error(self, field, error):
if field is not None:
raise TypeError(
"The argument `field` must be `None` when the `error` "
"argument contains errors for multiple fields."
"argument is a dictionary."
)
else:
error = error.error_dict
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ Models
(:attr:`~django.db.models.GeneratedField.db_persist` set to ``True``) on
Oracle 23ai/26ai (23.7+).

* The :data:`~django.db.models.signals.m2m_changed` signal now receives a
``raw`` argument.

Pagination
~~~~~~~~~~

Expand Down
7 changes: 7 additions & 0 deletions tests/forms_tests/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1660,6 +1660,13 @@ def clean(self):
with self.assertRaisesMessage(ValueError, "has no field named"):
f.add_error("missing_field", "Some error.")

with self.assertRaisesMessage(
TypeError,
"The argument `field` must be `None` when the `error` argument is a "
"dictionary.",
):
f.add_error("password1", ValidationError({"password1": "Some error."}))

def test_update_error_dict(self):
class CodeForm(Form):
code = CharField(max_length=10)
Expand Down
47 changes: 47 additions & 0 deletions tests/migrations/test_autodetector.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,13 @@ class AutodetectorTests(BaseAutodetectorTests):
("publishers", models.ManyToManyField("testapp.Publisher", blank=True)),
],
)
other_publisher = ModelState(
"testapp",
"OtherPublisher",
[
("id", models.AutoField(primary_key=True)),
],
)
author_with_m2m_through = ModelState(
"testapp",
"Author",
Expand Down Expand Up @@ -4557,6 +4564,46 @@ def test_many_to_many_changed_to_concrete_field(self):
)
self.assertOperationFieldAttributes(changes, "testapp", 0, 2, max_length=100)

def test_m2m_target_change_generates_remove_and_add(self):
before = [
self.publisher,
self.other_publisher,
self.author_with_m2m, # m2m to self.publisher.
]

after = [
self.publisher,
self.other_publisher,
ModelState(
"testapp",
"Author",
[
("id", models.AutoField(primary_key=True)),
# Repoint m2m to self.other_publisher.
("publishers", models.ManyToManyField("testapp.OtherPublisher")),
],
),
]
changes = self.get_changes(before, after)
self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(changes, "testapp", 0, ["RemoveField", "AddField"])
self.assertOperationAttributes(
changes,
"testapp",
0,
0,
name="publishers",
model_name="author",
)
self.assertOperationAttributes(
changes,
"testapp",
0,
1,
name="publishers",
model_name="author",
)

def test_non_circular_foreignkey_dependency_removal(self):
"""
If two models with a ForeignKey from one to the other are removed at
Expand Down