From 12385a5f868ee697266756511a85434627ae7ca6 Mon Sep 17 00:00:00 2001
From: Robert Stapenhurst <rob@secondsync.com>
Date: Sun, 9 Feb 2014 13:54:46 +0000
Subject: [PATCH] Fixed #21763 -- Added an error msg for missing methods on
 ManyRelatedManager.

Attempting to add() and remove() an object related by a 'through' model
now raises more descriptive AttributeErrors, in line with set and
create().
---
 django/db/models/fields/related.py | 33 +++++++++-----
 tests/m2m_through/tests.py         | 71 +++++++++++++++++++++++-------
 2 files changed, 77 insertions(+), 27 deletions(-)

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.