diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 75e5f20fea..6bd91817dd 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -867,20 +867,29 @@ def create_many_related_manager(superclass, rel): False, self.prefetch_cache_name) - # If the ManyToMany relation has an intermediary model, - # the add and remove methods do not exist. - if rel.through._meta.auto_created: - def add(self, *objs): - self._add_items(self.source_field_name, self.target_field_name, *objs) + def add(self, *objs): + if not rel.through._meta.auto_created: + opts = self.through._meta + raise AttributeError( + "Cannot use add() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % + (opts.app_label, opts.object_name) + ) + self._add_items(self.source_field_name, self.target_field_name, *objs) - # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table - if self.symmetrical: - self._add_items(self.target_field_name, self.source_field_name, *objs) - add.alters_data = True + # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table + if self.symmetrical: + self._add_items(self.target_field_name, self.source_field_name, *objs) + add.alters_data = True - def remove(self, *objs): - self._remove_items(self.source_field_name, self.target_field_name, *objs) - remove.alters_data = True + def remove(self, *objs): + if not rel.through._meta.auto_created: + opts = self.through._meta + raise AttributeError( + "Cannot use remove() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % + (opts.app_label, opts.object_name) + ) + self._remove_items(self.source_field_name, self.target_field_name, *objs) + remove.alters_data = True def clear(self): db = router.db_for_write(self.through, instance=self.instance) diff --git a/tests/m2m_through/tests.py b/tests/m2m_through/tests.py index 1e1b32f9eb..0fa3d4aa55 100644 --- a/tests/m2m_through/tests.py +++ b/tests/m2m_through/tests.py @@ -68,12 +68,24 @@ class M2mThroughTests(TestCase): def test_forward_descriptors(self): # Due to complications with adding via an intermediary model, - # the add method is not provided. - self.assertRaises(AttributeError, lambda: self.rock.members.add(self.bob)) + # the add method raises an error. + self.assertRaisesMessage( + AttributeError, + 'Cannot use add() on a ManyToManyField which specifies an intermediary model', + lambda: self.rock.members.add(self.bob) + ) # Create is also disabled as it suffers from the same problems as add. - self.assertRaises(AttributeError, lambda: self.rock.members.create(name='Anne')) - # Remove has similar complications, and is not provided either. - self.assertRaises(AttributeError, lambda: self.rock.members.remove(self.jim)) + self.assertRaisesMessage( + AttributeError, + 'Cannot use create() on a ManyToManyField which specifies an intermediary model', + lambda: self.rock.members.create(name='Anne') + ) + # Remove has similar complications, and it also raises an error. + self.assertRaisesMessage( + AttributeError, + 'Cannot use remove() on a ManyToManyField which specifies an intermediary model', + lambda: self.rock.members.remove(self.jim) + ) m1 = Membership.objects.create(person=self.jim, group=self.rock) m2 = Membership.objects.create(person=self.jane, group=self.rock) @@ -93,9 +105,17 @@ class M2mThroughTests(TestCase): [] ) - # Assignment should not work with models specifying a through model for many of - # the same reasons as adding. - self.assertRaises(AttributeError, setattr, self.rock, "members", backup) + # Assignment should not work with models specifying a through model for + # many of the same reasons as adding. + self.assertRaisesMessage( + AttributeError, + 'Cannot set values on a ManyToManyField which specifies an intermediary model', + setattr, + self.rock, + "members", + backup + ) + # Let's re-save those instances that we've cleared. m1.save() m2.save() @@ -111,11 +131,25 @@ class M2mThroughTests(TestCase): def test_reverse_descriptors(self): # Due to complications with adding via an intermediary model, # the add method is not provided. - self.assertRaises(AttributeError, lambda: self.bob.group_set.add(self.rock)) + self.assertRaisesMessage( + AttributeError, + 'Cannot use add() on a ManyToManyField which specifies an intermediary model', + lambda: self.bob.group_set.add(self.rock) + ) + # Create is also disabled as it suffers from the same problems as add. - self.assertRaises(AttributeError, lambda: self.bob.group_set.create(name="funk")) + self.assertRaisesMessage( + AttributeError, + 'Cannot use create() on a ManyToManyField which specifies an intermediary model', + lambda: self.bob.group_set.create(name="funk") + ) + # Remove has similar complications, and is not provided either. - self.assertRaises(AttributeError, lambda: self.jim.group_set.remove(self.rock)) + self.assertRaisesMessage( + AttributeError, + 'Cannot use remove() on a ManyToManyField which specifies an intermediary model', + lambda: self.jim.group_set.remove(self.rock) + ) m1 = Membership.objects.create(person=self.jim, group=self.rock) m2 = Membership.objects.create(person=self.jim, group=self.roll) @@ -133,11 +167,18 @@ class M2mThroughTests(TestCase): self.jim.group_set.all(), [] ) - # Assignment should not work with models specifying a through model for many of - # the same reasons as adding. - self.assertRaises(AttributeError, setattr, self.jim, "group_set", backup) - # Let's re-save those instances that we've cleared. + # Assignment should not work with models specifying a through model for + # many of the same reasons as adding. + self.assertRaisesMessage( + AttributeError, + 'Cannot set values on a ManyToManyField which specifies an intermediary model', + setattr, + self.jim, + "group_set", + backup + ) + # Let's re-save those instances that we've cleared. m1.save() m2.save() # Verifying that those instances were re-saved successfully.