Code

Ticket #6095: 6095-trunk-withdocs.rc1.diff

File 6095-trunk-withdocs.rc1.diff, 29.8 KB (added by floguy, 6 years ago)

Updated to latest trunk, with some syntactic improvements and taking into account latest suggestions.

Line 
1diff --git a/AUTHORS b/AUTHORS
2index b20092e..33bfed4 100644
3--- a/AUTHORS
4+++ b/AUTHORS
5@@ -144,6 +144,7 @@ answer newbie questions, and generally made Django that much better:
6     Afonso Fernández Nogueira <fonzzo.django@gmail.com>
7     Matthew Flanagan <http://wadofstuff.blogspot.com>
8     Eric Floehr <eric@intellovations.com>
9+    Eric Florenzano <floguy@gmail.com>
10     Vincent Foley <vfoleybourgon@yahoo.ca>
11     Rudolph Froger <rfroger@estrate.nl>
12     Jorge Gajon <gajon@gajon.org>
13diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
14index e91be70..2ff5858 100644
15--- a/django/contrib/contenttypes/generic.py
16+++ b/django/contrib/contenttypes/generic.py
17@@ -104,6 +104,9 @@ class GenericRelation(RelatedField, Field):
18                             limit_choices_to=kwargs.pop('limit_choices_to', None),
19                             symmetrical=kwargs.pop('symmetrical', True))
20 
21+        # By its very nature, a GenericRelation doesn't create a table.
22+        self.creates_table = False
23+
24         # Override content-type/object-id field names on the related class
25         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
26         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
27diff --git a/django/core/management/sql.py b/django/core/management/sql.py
28index 574be5a..2ebc626 100644
29--- a/django/core/management/sql.py
30+++ b/django/core/management/sql.py
31@@ -353,7 +353,7 @@ def many_to_many_sql_for_model(model, style):
32     qn = connection.ops.quote_name
33     inline_references = connection.features.inline_fk_references
34     for f in opts.local_many_to_many:
35-        if not isinstance(f.rel, generic.GenericRel):
36+        if f.creates_table:
37             tablespace = f.db_tablespace or opts.db_tablespace
38             if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
39                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
40diff --git a/django/core/management/validation.py b/django/core/management/validation.py
41index cd1f84f..b1fc5e0 100644
42--- a/django/core/management/validation.py
43+++ b/django/core/management/validation.py
44@@ -104,6 +104,7 @@ def get_validation_errors(outfile, app=None):
45                         if r.get_accessor_name() == rel_query_name:
46                             e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
47 
48+        seen_intermediary_signatures = []
49         for i, f in enumerate(opts.local_many_to_many):
50             # Check to see if the related m2m field will clash with any
51             # existing fields, m2m fields, m2m related objects or related
52@@ -114,7 +115,29 @@ def get_validation_errors(outfile, app=None):
53                 # so skip the next section
54                 if isinstance(f.rel.to, (str, unicode)):
55                     continue
56-
57+            if getattr(f.rel, 'through', None) is not None:
58+                intermediary_model = None
59+                for model in models.get_models():
60+                    if model._meta.module_name == f.rel.through.lower():
61+                        intermediary_model = model
62+                if intermediary_model is None:
63+                    e.add(opts, "%s has a manually-defined m2m relation through model %s, which does not exist." % (f.name, f.rel.through))
64+                else:
65+                    signature = (f.rel.to, cls, intermediary_model)
66+                    if signature in seen_intermediary_signatures:
67+                        e.add(opts, "%s has two manually-defined m2m relations through model %s, which is not possible.  Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, intermediary_model._meta.object_name))
68+                    else:
69+                        seen_intermediary_signatures.append(signature)
70+                    seen_related_fk, seen_this_fk = False, False
71+                    for field in intermediary_model._meta.fields:
72+                        if field.rel:
73+                            if field.rel.to == f.rel.to:
74+                                seen_related_fk = True
75+                            elif field.rel.to == cls:
76+                                seen_this_fk = True
77+                    if not seen_related_fk or not seen_this_fk:
78+                        e.add(opts, "%s has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name))
79+           
80             rel_opts = f.rel.to._meta
81             rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
82             rel_query_name = f.related_query_name()
83diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
84index 4ea0c3f..c30fedd 100644
85--- a/django/db/models/fields/related.py
86+++ b/django/db/models/fields/related.py
87@@ -335,7 +335,7 @@ class ForeignRelatedObjectsDescriptor(object):
88             manager.clear()
89         manager.add(*value)
90 
91-def create_many_related_manager(superclass):
92+def create_many_related_manager(superclass, through=False):
93     """Creates a manager that subclasses 'superclass' (which is a Manager)
94     and adds behavior for many-to-many related objects."""
95     class ManyRelatedManager(superclass):
96@@ -349,6 +349,7 @@ def create_many_related_manager(superclass):
97             self.join_table = join_table
98             self.source_col_name = source_col_name
99             self.target_col_name = target_col_name
100+            self.through = through
101             self._pk_val = self.instance._get_pk_val()
102             if self._pk_val is None:
103                 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model)
104@@ -381,6 +382,10 @@ def create_many_related_manager(superclass):
105         clear.alters_data = True
106 
107         def create(self, **kwargs):
108+            # This check needs to be done here, since we can't later remove this
109+            # from the method lookup table, as we do with add and remove.
110+            if through is not None:
111+                raise AttributeError, "'ManyRelatedManager' object has no attribute 'create'"
112             new_obj = self.model(**kwargs)
113             new_obj.save()
114             self.add(new_obj)
115@@ -448,6 +453,13 @@ def create_many_related_manager(superclass):
116                 [self._pk_val])
117             transaction.commit_unless_managed()
118 
119+    # If it's an intermediary model, detach the add and remove methods from this
120+    # ManyRelatedManager.  Create cannot be detached this way due to an
121+    # inherited method in the dynamic method lookup table that gets in the way.
122+    if through is not None:
123+        del ManyRelatedManager.add
124+        del ManyRelatedManager.remove
125+
126     return ManyRelatedManager
127 
128 class ManyRelatedObjectsDescriptor(object):
129@@ -468,7 +480,7 @@ class ManyRelatedObjectsDescriptor(object):
130         # model's default manager.
131         rel_model = self.related.model
132         superclass = rel_model._default_manager.__class__
133-        RelatedManager = create_many_related_manager(superclass)
134+        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
135 
136         qn = connection.ops.quote_name
137         manager = RelatedManager(
138@@ -487,6 +499,10 @@ class ManyRelatedObjectsDescriptor(object):
139         if instance is None:
140             raise AttributeError, "Manager must be accessed via instance"
141 
142+        through = getattr(self.related.field.rel, 'through', None)
143+        if through is not None:
144+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through
145+
146         manager = self.__get__(instance)
147         manager.clear()
148         manager.add(*value)
149@@ -509,7 +525,7 @@ class ReverseManyRelatedObjectsDescriptor(object):
150         # model's default manager.
151         rel_model=self.field.rel.to
152         superclass = rel_model._default_manager.__class__
153-        RelatedManager = create_many_related_manager(superclass)
154+        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
155 
156         qn = connection.ops.quote_name
157         manager = RelatedManager(
158@@ -528,6 +544,10 @@ class ReverseManyRelatedObjectsDescriptor(object):
159         if instance is None:
160             raise AttributeError, "Manager must be accessed via instance"
161 
162+        through = getattr(self.field.rel, 'through', None)
163+        if through is not None:
164+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through
165+
166         manager = self.__get__(instance)
167         manager.clear()
168         manager.add(*value)
169@@ -581,7 +601,7 @@ class OneToOneRel(ManyToOneRel):
170 
171 class ManyToManyRel(object):
172     def __init__(self, to, num_in_admin=0, related_name=None,
173-        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True):
174+        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True, through=None):
175         self.to = to
176         self.num_in_admin = num_in_admin
177         self.related_name = related_name
178@@ -593,6 +613,7 @@ class ManyToManyRel(object):
179         self.raw_id_admin = raw_id_admin
180         self.symmetrical = symmetrical
181         self.multiple = True
182+        self.through = through
183 
184         assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"
185 
186@@ -736,8 +757,14 @@ class ManyToManyField(RelatedField, Field):
187             filter_interface=kwargs.pop('filter_interface', None),
188             limit_choices_to=kwargs.pop('limit_choices_to', None),
189             raw_id_admin=kwargs.pop('raw_id_admin', False),
190-            symmetrical=kwargs.pop('symmetrical', True))
191+            symmetrical=kwargs.pop('symmetrical', True),
192+            through=kwargs.pop('through', None))
193         self.db_table = kwargs.pop('db_table', None)
194+        if kwargs['rel'].through is not None:
195+            self.creates_table = False
196+            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
197+        else:
198+            self.creates_table = True
199         if kwargs["rel"].raw_id_admin:
200             kwargs.setdefault("validator_list", []).append(self.isValidIDList)
201         Field.__init__(self, **kwargs)
202@@ -760,7 +787,9 @@ class ManyToManyField(RelatedField, Field):
203 
204     def _get_m2m_db_table(self, opts):
205         "Function that can be curried to provide the m2m table name for this relation"
206-        if self.db_table:
207+        if self.rel.through is not None:
208+            return get_model(opts.app_label, self.rel.through)._meta.db_table
209+        elif self.db_table:
210             return self.db_table
211         else:
212             return '%s_%s' % (opts.db_table, self.name)
213@@ -768,7 +797,10 @@ class ManyToManyField(RelatedField, Field):
214     def _get_m2m_column_name(self, related):
215         "Function that can be curried to provide the source column name for the m2m table"
216         # If this is an m2m relation to self, avoid the inevitable name clash
217-        if related.model == related.parent_model:
218+        if self.rel.through is not None:
219+            field = related.model._meta.get_related_object(self.rel.through).field
220+            return field.get_attname_column()[1]
221+        elif related.model == related.parent_model:
222             return 'from_' + related.model._meta.object_name.lower() + '_id'
223         else:
224             return related.model._meta.object_name.lower() + '_id'
225@@ -776,7 +808,10 @@ class ManyToManyField(RelatedField, Field):
226     def _get_m2m_reverse_name(self, related):
227         "Function that can be curried to provide the related column name for the m2m table"
228         # If this is an m2m relation to self, avoid the inevitable name clash
229-        if related.model == related.parent_model:
230+        if self.rel.through is not None:
231+            field = related.parent_model._meta.get_related_object(self.rel.through).field
232+            return field.get_attname_column()[1]
233+        elif related.model == related.parent_model:
234             return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
235         else:
236             return related.parent_model._meta.object_name.lower() + '_id'
237diff --git a/django/db/models/options.py b/django/db/models/options.py
238index 5802ead..10056a1 100644
239--- a/django/db/models/options.py
240+++ b/django/db/models/options.py
241@@ -10,7 +10,7 @@ from django.db.models.related import RelatedObject
242 from django.db.models.fields.related import ManyToManyRel
243 from django.db.models.fields import AutoField, FieldDoesNotExist
244 from django.db.models.fields.proxy import OrderWrt
245-from django.db.models.loading import get_models, app_cache_ready
246+from django.db.models.loading import get_models, get_model, app_cache_ready
247 from django.db.models import Manager
248 from django.utils.translation import activate, deactivate_all, get_language, string_concat
249 from django.utils.encoding import force_unicode, smart_str
250@@ -374,6 +374,15 @@ class Options(object):
251             follow = self.get_follow()
252         return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
253 
254+    def get_related_object(self, from_model):
255+        "Gets the RelatedObject which links from from_model to this model."
256+        if isinstance(from_model, str):
257+            from_model = get_model(self.app_label, from_model)
258+        for related_object in self.get_all_related_objects():
259+            if related_object.model == from_model:
260+                return related_object
261+        return None
262+
263     def get_data_holders(self, follow=None):
264         if follow == None:
265             follow = self.get_follow()
266diff --git a/docs/model-api.txt b/docs/model-api.txt
267index 4ed4ede..d96710d 100644
268--- a/docs/model-api.txt
269+++ b/docs/model-api.txt
270@@ -991,6 +991,83 @@ the relationship should work. All are optional:
271 
272     =======================  ============================================================
273 
274+Extra fields on many-to-many relationships
275+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
276+
277+When you're only dealing with mixing and matching pizzas and toppings, a standard
278+``ManyToManyField`` works great.  For many situations, however, some extra
279+fields are required on the relationships between models.  For situations like
280+this, Django allows for the specification of an intermediary many-to-many model.
281+To enable this functionality, simply specify a ``through`` keyword argument onto
282+the ``ManyToManyField``.  This is best illustrated with an example::
283+
284+    class Person(models.Model):
285+        # ...
286+        name = models.CharField(max_length=128)
287+
288+        def __unicode__(self):
289+            return self.name
290+
291+    class Group(models.Model):
292+        # ...
293+        name = models.CharField(max_length=128)
294+        members = models.ManyToManyField(Person, through='Membership')
295+
296+        def __unicode__(self):
297+            return self.name
298+
299+    class Membership(models.Model):
300+        person = models.ForeignKey(Person)
301+        group = models.ForeignKey(Group)
302+        date_joined = models.DateTimeField()
303+        invite_reason = models.CharField(max_length=64)
304+
305+.. note::
306+
307+    The intermediary model must provide foreign keys to both of the models in
308+    the relation.  This explicit declaration makes it clear how two models are
309+    related.  More importantly, perhaps, these foreign keys also provide access
310+    to the intermediary model from either of the other models in the relation
311+    (as with any other foreign key).
312+
313+Now that you have set up your ``ManyToManyField`` to use your intermediary
314+model (Membership, in this case), you're ready to use the convenience methods
315+provided by that ``ManyToManyField``.  Here's an example of how you can query
316+for and use these models::
317+   
318+    >>> ringo = Person.objects.create(name="Ringo Starr")
319+    >>> paul = Person.objects.create(name="Paul McCartney")
320+    >>> beatles = Group.objects.create(name="The Beatles")
321+    >>> m1 = Membership.objects.create(person=ringo, group=beatles,
322+    ...     date_joined=datetime(1962, 8, 16),
323+    ...     invite_reason= "Needed a new drummer.")
324+    >>> beatles.members.all()
325+    [<Person: Ringo Starr>]
326+    >>> ringo.group_set.all()
327+    [<Group: The Beatles>]
328+    >>> m2 = Membership.objects.create(person=paul, group=beatles,
329+    ...     date_joined=datetime(1960, 8, 1),
330+    ...     invite_reason= "Wanted to form a band.")
331+    >>> beatles.members.all()
332+    [<Person: Ringo Starr>, <Person: Paul McCartney>]
333+
334+As you can see, creating ``Membership`` objects automatically adds the
335+``Person`` objects to the ``beatles.members`` queryset.  This means that you
336+can do anything that you would do on a normal queryset, like ``filter`` or
337+``exclude``.
338+
339+.. note::
340+
341+    As soon as an intermediary model is specified, the ``add`` and 
342+    ``remove`` methods become unavailable on the descriptors added by the
343+    ``ManyToManyField``.  For example, something like 
344+    ``beatles.members.add(paul)`` will no longer work.
345+
346+For more examples and ideas on how to work with intermediary models, 
347+`see the tests`_.
348+
349+.. _`see the tests`: http://code.djangoproject.com/browser/django/trunk/tests/modeltests/m2m_manual/models.py
350+
351 One-to-one relationships
352 ~~~~~~~~~~~~~~~~~~~~~~~~
353 
354diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
355index 8a480a2..c80670d 100644
356--- a/tests/modeltests/invalid_models/models.py
357+++ b/tests/modeltests/invalid_models/models.py
358@@ -111,6 +111,31 @@ class Car(models.Model):
359 class MissingRelations(models.Model):
360     rel1 = models.ForeignKey("Rel1")
361     rel2 = models.ManyToManyField("Rel2")
362+   
363+class MissingManualM2MModel(models.Model):
364+    name = models.CharField(max_length=5)
365+    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
366+   
367+class Person(models.Model):
368+    name = models.CharField(max_length=5)
369+
370+class Group(models.Model):
371+    name = models.CharField(max_length=5)
372+    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
373+    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
374+
375+class GroupTwo(models.Model):
376+    name = models.CharField(max_length=5)
377+    primary = models.ManyToManyField(Person, through="Membership")
378+    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
379+
380+class Membership(models.Model):
381+    person = models.ForeignKey(Person)
382+    group = models.ForeignKey(Group)
383+    not_default_or_null = models.CharField(max_length=5)
384+
385+class MembershipMissingFK(models.Model):
386+    person = models.ForeignKey(Person)
387 
388 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
389 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
390@@ -197,4 +222,8 @@ invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi
391 invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'.
392 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
393 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
394+invalid_models.group: Group has two manually-defined m2m relations through model Membership, which is not possible.  Please consider using an extra field on your intermediary model instead.
395+invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relation through model MissingM2MModel, which does not exist.
396+invalid_models.grouptwo: primary has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
397+invalid_models.grouptwo: secondary has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
398 """
399diff --git a/tests/modeltests/m2m_manual/__init__.py b/tests/modeltests/m2m_manual/__init__.py
400new file mode 100644
401index 0000000..8b13789
402--- /dev/null
403+++ b/tests/modeltests/m2m_manual/__init__.py
404@@ -0,0 +1 @@
405+
406diff --git a/tests/modeltests/m2m_manual/models.py b/tests/modeltests/m2m_manual/models.py
407new file mode 100644
408index 0000000..a8c7e05
409--- /dev/null
410+++ b/tests/modeltests/m2m_manual/models.py
411@@ -0,0 +1,278 @@
412+from django.db import models
413+from datetime import datetime
414+
415+# M2M described on one of the models
416+class Person(models.Model):
417+    name = models.CharField(max_length=128)
418+
419+    def __unicode__(self):
420+        return self.name
421+
422+class Group(models.Model):
423+    name = models.CharField(max_length=128)
424+    members = models.ManyToManyField(Person, through='Membership')
425+    custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom")
426+    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
427+   
428+    def __unicode__(self):
429+        return self.name
430+
431+class Membership(models.Model):
432+    person = models.ForeignKey(Person)
433+    group = models.ForeignKey(Group)
434+    date_joined = models.DateTimeField(default=datetime.now)
435+    invite_reason = models.CharField(max_length=64, null=True)
436+   
437+    def __unicode__(self):
438+        return "%s is a member of %s" % (self.person.name, self.group.name)
439+
440+class CustomMembership(models.Model):
441+    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
442+    group = models.ForeignKey(Group)
443+    weird_fk = models.ForeignKey(Membership, null=True)
444+    date_joined = models.DateTimeField(default=datetime.now)
445+   
446+    def __unicode__(self):
447+        return "%s is a member of %s" % (self.person.name, self.group.name)
448+   
449+    class Meta:
450+        db_table = "test_table"
451+
452+class TestNoDefaultsOrNulls(models.Model):
453+    person = models.ForeignKey(Person)
454+    group = models.ForeignKey(Group)
455+    nodefaultnonull = models.CharField(max_length=5)
456+
457+__test__ = {'API_TESTS':"""
458+>>> from datetime import datetime
459+
460+### Creation and Saving Tests ###
461+
462+>>> bob = Person.objects.create(name = 'Bob')
463+>>> jim = Person.objects.create(name = 'Jim')
464+>>> jane = Person.objects.create(name = 'Jane')
465+>>> rock = Group.objects.create(name = 'Rock')
466+>>> roll = Group.objects.create(name = 'Roll')
467+
468+# We start out by making sure that the Group 'rock' has no members.
469+>>> rock.members.all()
470+[]
471+
472+# To make Jim a member of Group Rock, simply create a Membership object.
473+>>> m1 = Membership.objects.create(person = jim, group = rock)
474+
475+# We can do the same for Jane and Rock.
476+>>> m2 = Membership.objects.create(person = jane, group = rock)
477+
478+# Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
479+>>> rock.members.all()
480+[<Person: Jim>, <Person: Jane>]
481+
482+# Now we can add a bunch more Membership objects to test with.
483+>>> m3 = Membership.objects.create(person = bob, group = roll)
484+>>> m4 = Membership.objects.create(person = jim, group = roll)
485+>>> m5 = Membership.objects.create(person = jane, group = roll)
486+
487+# We can get Jim's Group membership as with any ForeignKey.
488+>>> jim.group_set.all()
489+[<Group: Rock>, <Group: Roll>]
490+
491+# Querying the intermediary model works like normal. 
492+# In this case we get Jane's membership to Rock.
493+>>> m = Membership.objects.get(person = jane, group = rock)
494+>>> m
495+<Membership: Jane is a member of Rock>
496+
497+# Now we set some date_joined dates for further testing.
498+>>> m2.invite_reason = "She was just awesome."
499+>>> m2.date_joined = datetime(2006, 1, 1)
500+>>> m2.save()
501+
502+>>> m5.date_joined = datetime(2004, 1, 1)
503+>>> m5.save()
504+
505+>>> m3.date_joined = datetime(2004, 1, 1)
506+>>> m3.save()
507+
508+# It's not only get that works.  Filter works like normal as well.
509+>>> Membership.objects.filter(person = jim)
510+[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
511+
512+
513+### Forward Descriptors Tests ###
514+
515+# Due to complications with adding via an intermediary model, the add method is
516+# not provided.
517+>>> rock.members.add(bob)
518+Traceback (most recent call last):
519+...
520+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
521+
522+>>> rock.members.create(name = 'Anne')
523+Traceback (most recent call last):
524+...
525+AttributeError: 'ManyRelatedManager' object has no attribute 'create'
526+
527+# Remove has similar complications, and is not provided either.
528+>>> rock.members.remove(jim)
529+Traceback (most recent call last):
530+...
531+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
532+
533+# Here we back up the list of all members of Rock.
534+>>> backup = list(rock.members.all())
535+
536+# ...and we verify that it has worked.
537+>>> backup
538+[<Person: Jim>, <Person: Jane>]
539+
540+# The clear function should still work.
541+>>> rock.members.clear()
542+
543+# Now there will be no members of Rock.
544+>>> rock.members.all()
545+[]
546+
547+# Assignment should not work with models specifying a through model for many of
548+# the same reasons as adding.
549+>>> rock.members = backup
550+Traceback (most recent call last):
551+...
552+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
553+
554+# Let's re-save those instances that we've cleared.
555+>>> m1.save()
556+>>> m2.save()
557+
558+# Verifying that those instances were re-saved successfully.
559+>>> rock.members.all()
560+[<Person: Jim>, <Person: Jane>]
561+
562+
563+### Reverse Descriptors Tests ###
564+
565+# Due to complications with adding via an intermediary model, the add method is
566+# not provided.
567+>>> bob.group_set.add(rock)
568+Traceback (most recent call last):
569+...
570+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
571+
572+# Create is another method that should not work correctly, as it suffers from
573+# the same problems as add.
574+>>> bob.group_set.create(name = 'Funk')
575+Traceback (most recent call last):
576+...
577+AttributeError: 'ManyRelatedManager' object has no attribute 'create'
578+
579+# Remove has similar complications, and is not provided either.
580+>>> jim.group_set.remove(rock)
581+Traceback (most recent call last):
582+...
583+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
584+
585+# Here we back up the list of all of Jim's groups.
586+>>> backup = list(jim.group_set.all())
587+>>> backup
588+[<Group: Rock>, <Group: Roll>]
589+
590+# The clear function should still work.
591+>>> jim.group_set.clear()
592+
593+# Now Jim will be in no groups.
594+>>> jim.group_set.all()
595+[]
596+
597+# Assignment should not work with models specifying a through model for many of
598+# the same reasons as adding.
599+>>> jim.group_set = backup
600+Traceback (most recent call last):
601+...
602+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
603+
604+# Let's re-save those instances that we've cleared.
605+>>> m1.save()
606+>>> m4.save()
607+
608+# Verifying that those instances were re-saved successfully.
609+>>> jim.group_set.all()
610+[<Group: Rock>, <Group: Roll>]
611+
612+### Custom Tests ###
613+
614+# Let's see if we can query through our second relationship.
615+>>> rock.custom_members.all()
616+[]
617+
618+# We can query in the opposite direction as well.
619+>>> bob.custom.all()
620+[]
621+
622+# Let's create some membership objects in this custom relationship.
623+>>> cm1 = CustomMembership.objects.create(person = bob, group = rock)
624+>>> cm2 = CustomMembership.objects.create(person = jim, group = rock)
625+
626+# If we get the number of people in Rock, it should be both Bob and Jim.
627+>>> rock.custom_members.all()
628+[<Person: Bob>, <Person: Jim>]
629+
630+# Bob should only be in one custom group.
631+>>> bob.custom.all()
632+[<Group: Rock>]
633+
634+# Let's make sure our new descriptors don't conflict with the FK related_name.
635+>>> bob.custom_person_related_name.all()
636+[<CustomMembership: Bob is a member of Rock>]
637+
638+### QUERY TESTS ###
639+
640+# We can query for the related model by using its attribute name (members, in
641+# this case).
642+>>> Group.objects.filter(members__name='Bob')
643+[<Group: Roll>]
644+
645+# To query through the intermediary model, we specify its model name.
646+# In this case, membership.
647+>>> Group.objects.filter(membership__invite_reason = "She was just awesome.")
648+[<Group: Rock>]
649+
650+# If we want to query in the reverse direction by the related model, use its
651+# model name (group, in this case).
652+>>> Person.objects.filter(group__name="Rock")
653+[<Person: Jim>, <Person: Jane>]
654+
655+# If the m2m field has specified a related_name, using that will work.
656+>>> Person.objects.filter(custom__name="Rock")
657+[<Person: Bob>, <Person: Jim>]
658+
659+# To query through the intermediary model in the reverse direction, we again
660+# specify its model name (membership, in this case).
661+>>> Person.objects.filter(membership__invite_reason = "She was just awesome.")
662+[<Person: Jane>]
663+
664+# Let's see all of the groups that Jane joined after 1 Jan 2005:
665+>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
666+... membership__person = jane)
667+[<Group: Rock>]
668+
669+# Queries also work in the reverse direction: Now let's see all of the people
670+# that have joined Rock since 1 Jan 2005:
671+>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
672+... membership__group = rock)
673+[<Person: Jim>, <Person: Jane>]
674+
675+# Conceivably, queries through membership could return correct, but non-unique
676+# querysets.  To demonstrate this, we query for all people who have joined a
677+# group after 2004:
678+>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1))
679+[<Person: Jim>, <Person: Jim>, <Person: Jane>]
680+
681+# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
682+>>> [(m.person.name, m.group.name) for m in
683+... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))]
684+[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
685+
686+# QuerySet's distinct() method can correct this problem.
687+>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct()
688+[<Person: Jim>, <Person: Jane>]
689+"""}
690\ No newline at end of file