Code

Ticket #6095: 6095-beta-06.diff

File 6095-beta-06.diff, 27.5 KB (added by floguy, 6 years ago)

Added/reworded lots of the comments to the m2m_manual doctests (maybe these tests need a rename, hmmm), and fixed some issues with the .diff that prevented parts from showing up last time.

Line 
1diff --git a/AUTHORS b/AUTHORS
2index be18d21..49a5b31 100644
3--- a/AUTHORS
4+++ b/AUTHORS
5@@ -133,6 +133,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/core/management/sql.py b/django/core/management/sql.py
14index 15bffce..5430dea 100644
15--- a/django/core/management/sql.py
16+++ b/django/core/management/sql.py
17@@ -352,7 +352,7 @@ def many_to_many_sql_for_model(model, style):
18     qn = connection.ops.quote_name
19     inline_references = connection.features.inline_fk_references
20     for f in opts.many_to_many:
21-        if not isinstance(f.rel, generic.GenericRel):
22+        if not isinstance(f.rel, generic.GenericRel) and f.creates_table:
23             tablespace = f.db_tablespace or opts.db_tablespace
24             if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
25                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
26diff --git a/django/core/management/validation.py b/django/core/management/validation.py
27index bc9faae..a84cf35 100644
28--- a/django/core/management/validation.py
29+++ b/django/core/management/validation.py
30@@ -104,6 +104,8 @@ def get_validation_errors(outfile, app=None):
31                         if r.get_accessor_name() == rel_query_name:
32                             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))
33 
34+        seen_intermediary_signatures = []
35+
36         for i, f in enumerate(opts.many_to_many):
37             # Check to see if the related m2m field will clash with any
38             # existing fields, m2m fields, m2m related objects or related objects
39@@ -113,6 +115,28 @@ def get_validation_errors(outfile, app=None):
40                 # so skip the next section
41                 if isinstance(f.rel.to, (str, unicode)):
42                     continue
43+            if hasattr(f.rel, 'through') and f.rel.through != None:
44+                intermediary_model = None
45+                for model in models.get_models():
46+                    if model._meta.module_name == f.rel.through.lower():
47+                        intermediary_model = model
48+                if intermediary_model == None:
49+                    e.add(opts, "%s has a manually-defined m2m relationship through a model (%s) which does not exist." % (f.name, f.rel.through))
50+                else:
51+                    signature = (f.rel.to, cls, intermediary_model)
52+                    if signature in seen_intermediary_signatures:
53+                        e.add(opts, "%s has two manually-defined m2m relationships through the same model (%s), which is not possible.  Please use a field on your intermediary model instead." % (cls._meta.object_name, intermediary_model._meta.object_name))
54+                    else:
55+                        seen_intermediary_signatures.append(signature)
56+                    seen_related_fk, seen_this_fk = False, False
57+                    for field in intermediary_model._meta.fields:
58+                        if field.rel:
59+                            if field.rel.to == f.rel.to:
60+                                seen_related_fk = True
61+                            elif field.rel.to == cls:
62+                                seen_this_fk = True
63+                    if not seen_related_fk or not seen_this_fk:
64+                        e.add(opts, "%s has a manually-defined m2m relationship through a 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))
65 
66             rel_opts = f.rel.to._meta
67             rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
68diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
69index 7d28ba1..9da7073 100644
70--- a/django/db/models/fields/related.py
71+++ b/django/db/models/fields/related.py
72@@ -10,6 +10,7 @@ from django.core import validators
73 from django import oldforms
74 from django import newforms as forms
75 from django.dispatch import dispatcher
76+from new import instancemethod
77 
78 try:
79     set
80@@ -262,12 +263,13 @@ class ForeignRelatedObjectsDescriptor(object):
81             manager.clear()
82         manager.add(*value)
83 
84-def create_many_related_manager(superclass):
85+def create_many_related_manager(superclass, through=False):
86     """Creates a manager that subclasses 'superclass' (which is a Manager)
87     and adds behavior for many-to-many related objects."""
88     class ManyRelatedManager(superclass):
89         def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
90-                join_table=None, source_col_name=None, target_col_name=None):
91+                join_table=None, source_col_name=None, target_col_name=None,
92+                through=None):
93             super(ManyRelatedManager, self).__init__()
94             self.core_filters = core_filters
95             self.model = model
96@@ -276,6 +278,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@@ -283,22 +286,6 @@ def create_many_related_manager(superclass):
105         def get_query_set(self):
106             return superclass.get_query_set(self).filter(**(self.core_filters))
107 
108-        def add(self, *objs):
109-            self._add_items(self.source_col_name, self.target_col_name, *objs)
110-
111-            # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
112-            if self.symmetrical:
113-                self._add_items(self.target_col_name, self.source_col_name, *objs)
114-        add.alters_data = True
115-
116-        def remove(self, *objs):
117-            self._remove_items(self.source_col_name, self.target_col_name, *objs)
118-
119-            # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
120-            if self.symmetrical:
121-                self._remove_items(self.target_col_name, self.source_col_name, *objs)
122-        remove.alters_data = True
123-
124         def clear(self):
125             self._clear_items(self.source_col_name)
126 
127@@ -375,6 +362,24 @@ def create_many_related_manager(superclass):
128                 [self._pk_val])
129             transaction.commit_unless_managed()
130 
131+    def add(self, *objs):
132+        self._add_items(self.source_col_name, self.target_col_name, *objs)
133+        # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
134+        if self.symmetrical:
135+            self._add_items(self.target_col_name, self.source_col_name, *objs)
136+    add.alters_data = True
137+
138+    def remove(self, *objs):
139+        self._remove_items(self.source_col_name, self.target_col_name, *objs)
140+        # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
141+        if self.symmetrical:
142+            self._remove_items(self.target_col_name, self.source_col_name, *objs)
143+    remove.alters_data = True
144+
145+    if not through:
146+        ManyRelatedManager.add = instancemethod(add, None, ManyRelatedManager)
147+        ManyRelatedManager.remove = instancemethod(remove, None, ManyRelatedManager)
148+
149     return ManyRelatedManager
150 
151 class ManyRelatedObjectsDescriptor(object):
152@@ -395,7 +400,7 @@ class ManyRelatedObjectsDescriptor(object):
153         # model's default manager.
154         rel_model = self.related.model
155         superclass = rel_model._default_manager.__class__
156-        RelatedManager = create_many_related_manager(superclass)
157+        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
158 
159         qn = connection.ops.quote_name
160         manager = RelatedManager(
161@@ -405,7 +410,8 @@ class ManyRelatedObjectsDescriptor(object):
162             symmetrical=False,
163             join_table=qn(self.related.field.m2m_db_table()),
164             source_col_name=qn(self.related.field.m2m_reverse_name()),
165-            target_col_name=qn(self.related.field.m2m_column_name())
166+            target_col_name=qn(self.related.field.m2m_column_name()),
167+            through=self.related.field.rel.through
168         )
169 
170         return manager
171@@ -414,6 +420,10 @@ class ManyRelatedObjectsDescriptor(object):
172         if instance is None:
173             raise AttributeError, "Manager must be accessed via instance"
174 
175+        through = getattr(self.related.field.rel, 'through', None)
176+        if through:
177+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
178+       
179         manager = self.__get__(instance)
180         manager.clear()
181         manager.add(*value)
182@@ -436,7 +446,7 @@ class ReverseManyRelatedObjectsDescriptor(object):
183         # model's default manager.
184         rel_model=self.field.rel.to
185         superclass = rel_model._default_manager.__class__
186-        RelatedManager = create_many_related_manager(superclass)
187+        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
188 
189         qn = connection.ops.quote_name
190         manager = RelatedManager(
191@@ -446,7 +456,8 @@ class ReverseManyRelatedObjectsDescriptor(object):
192             symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model),
193             join_table=qn(self.field.m2m_db_table()),
194             source_col_name=qn(self.field.m2m_column_name()),
195-            target_col_name=qn(self.field.m2m_reverse_name())
196+            target_col_name=qn(self.field.m2m_reverse_name()),
197+            through=self.field.rel.through
198         )
199 
200         return manager
201@@ -455,6 +466,10 @@ class ReverseManyRelatedObjectsDescriptor(object):
202         if instance is None:
203             raise AttributeError, "Manager must be accessed via instance"
204 
205+        through = getattr(self.field.rel, 'through', None)
206+        if through:
207+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
208+
209         manager = self.__get__(instance)
210         manager.clear()
211         manager.add(*value)
212@@ -648,8 +663,14 @@ class ManyToManyField(RelatedField, Field):
213             filter_interface=kwargs.pop('filter_interface', None),
214             limit_choices_to=kwargs.pop('limit_choices_to', None),
215             raw_id_admin=kwargs.pop('raw_id_admin', False),
216-            symmetrical=kwargs.pop('symmetrical', True))
217+            symmetrical=kwargs.pop('symmetrical', True),
218+            through=kwargs.pop('through', None))
219         self.db_table = kwargs.pop('db_table', None)
220+        if kwargs['rel'].through:
221+            self.creates_table = False
222+            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used."
223+        else:
224+            self.creates_table = True
225         if kwargs["rel"].raw_id_admin:
226             kwargs.setdefault("validator_list", []).append(self.isValidIDList)
227         Field.__init__(self, **kwargs)
228@@ -672,7 +693,9 @@ class ManyToManyField(RelatedField, Field):
229 
230     def _get_m2m_db_table(self, opts):
231         "Function that can be curried to provide the m2m table name for this relation"
232-        if self.db_table:
233+        if self.rel.through != None:
234+            return get_model(opts.app_label, self.rel.through)._meta.db_table
235+        elif self.db_table:
236             return self.db_table
237         else:
238             return '%s_%s' % (opts.db_table, self.name)
239@@ -680,7 +703,11 @@ class ManyToManyField(RelatedField, Field):
240     def _get_m2m_column_name(self, related):
241         "Function that can be curried to provide the source column name for the m2m table"
242         # If this is an m2m relation to self, avoid the inevitable name clash
243-        if related.model == related.parent_model:
244+        if self.rel.through != None:
245+            field = related.model._meta.get_related_object(self.rel.through).field
246+            attname, column = field.get_attname_column()
247+            return column
248+        elif related.model == related.parent_model:
249             return 'from_' + related.model._meta.object_name.lower() + '_id'
250         else:
251             return related.model._meta.object_name.lower() + '_id'
252@@ -688,7 +715,11 @@ class ManyToManyField(RelatedField, Field):
253     def _get_m2m_reverse_name(self, related):
254         "Function that can be curried to provide the related column name for the m2m table"
255         # If this is an m2m relation to self, avoid the inevitable name clash
256-        if related.model == related.parent_model:
257+        if self.rel.through != None:
258+            field = related.parent_model._meta.get_related_object(self.rel.through).field
259+            attname, column = field.get_attname_column()
260+            return column
261+        elif related.model == related.parent_model:
262             return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
263         else:
264             return related.parent_model._meta.object_name.lower() + '_id'
265@@ -809,7 +840,8 @@ class OneToOneRel(ManyToOneRel):
266 
267 class ManyToManyRel(object):
268     def __init__(self, to, num_in_admin=0, related_name=None,
269-        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True):
270+        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True,
271+        through = None):
272         self.to = to
273         self.num_in_admin = num_in_admin
274         self.related_name = related_name
275@@ -821,5 +853,6 @@ class ManyToManyRel(object):
276         self.raw_id_admin = raw_id_admin
277         self.symmetrical = symmetrical
278         self.multiple = True
279+        self.through = through
280 
281         assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"
282diff --git a/django/db/models/options.py b/django/db/models/options.py
283index 37ace0a..f0af416 100644
284--- a/django/db/models/options.py
285+++ b/django/db/models/options.py
286@@ -2,7 +2,7 @@ from django.conf import settings
287 from django.db.models.related import RelatedObject
288 from django.db.models.fields.related import ManyToManyRel
289 from django.db.models.fields import AutoField, FieldDoesNotExist
290-from django.db.models.loading import get_models, app_cache_ready
291+from django.db.models.loading import get_models, get_model, app_cache_ready
292 from django.db.models.query import orderlist2sql
293 from django.db.models import Manager
294 from django.utils.translation import activate, deactivate_all, get_language, string_concat
295@@ -162,6 +162,15 @@ class Options(object):
296             follow = self.get_follow()
297         return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
298 
299+    def get_related_object(self, from_model):
300+        "Gets the RelatedObject which links from from_model to this model."
301+        if isinstance(from_model, str):
302+            from_model = get_model(self.app_label, from_model)
303+        for related_object in self.get_all_related_objects():
304+            if related_object.model == from_model:
305+                return related_object
306+        return None
307+
308     def get_data_holders(self, follow=None):
309         if follow == None:
310             follow = self.get_follow()
311diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
312index 8a480a2..191950d 100644
313--- a/tests/modeltests/invalid_models/models.py
314+++ b/tests/modeltests/invalid_models/models.py
315@@ -111,6 +111,31 @@ class Car(models.Model):
316 class MissingRelations(models.Model):
317     rel1 = models.ForeignKey("Rel1")
318     rel2 = models.ManyToManyField("Rel2")
319+   
320+class MissingManualM2MModel(models.Model):
321+    name = models.CharField(max_length=5)
322+    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
323+   
324+class Person(models.Model):
325+    name = models.CharField(max_length=5)
326+
327+class Group(models.Model):
328+    name = models.CharField(max_length=5)
329+    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
330+    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
331+
332+class GroupTwo(models.Model):
333+    name = models.CharField(max_length=5)
334+    primary = models.ManyToManyField(Person, through="Membership")
335+    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
336+
337+class Membership(models.Model):
338+    person = models.ForeignKey(Person)
339+    group = models.ForeignKey(Group)
340+    not_default_or_null = models.CharField(max_length=5)
341+
342+class MembershipMissingFK(models.Model):
343+    person = models.ForeignKey(Person)
344 
345 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
346 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
347@@ -197,4 +222,8 @@ invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi
348 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'.
349 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
350 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
351+invalid_models.group: Group has two manually-defined m2m relationships through the same model (Membership), which is not possible.  Please use a field on your intermediary model instead.
352+invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist.
353+invalid_models.grouptwo: primary has a manually-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo
354+invalid_models.grouptwo: secondary has a manually-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo
355 """
356diff --git a/tests/modeltests/m2m_manual/__init__.py b/tests/modeltests/m2m_manual/__init__.py
357new file mode 100644
358index 0000000..e69de29
359diff --git a/tests/modeltests/m2m_manual/models.py b/tests/modeltests/m2m_manual/models.py
360new file mode 100644
361index 0000000..089cf44
362--- /dev/null
363+++ b/tests/modeltests/m2m_manual/models.py
364@@ -0,0 +1,262 @@
365+from django.db import models
366+from datetime import datetime
367+
368+# M2M described on one of the models
369+class Person(models.Model):
370+    name = models.CharField(max_length=128)
371+
372+    def __unicode__(self):
373+        return self.name
374+
375+class Group(models.Model):
376+    name = models.CharField(max_length=128)
377+    members = models.ManyToManyField(Person, through='Membership')
378+    custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom")
379+    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
380+   
381+    def __unicode__(self):
382+        return self.name
383+
384+class Membership(models.Model):
385+    person = models.ForeignKey(Person)
386+    group = models.ForeignKey(Group)
387+    date_joined = models.DateTimeField(default=datetime.now)
388+    invite_reason = models.CharField(max_length=64, null=True)
389+   
390+    def __unicode__(self):
391+        return "%s is a member of %s" % (self.person.name, self.group.name)
392+
393+class CustomMembership(models.Model):
394+    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
395+    group = models.ForeignKey(Group)
396+    weird_fk = models.ForeignKey(Membership, null=True)
397+    date_joined = models.DateTimeField(default=datetime.now)
398+   
399+    def __unicode__(self):
400+        return "%s is a member of %s" % (self.person.name, self.group.name)
401+   
402+    class Meta:
403+        db_table = "test_table"
404+
405+class TestNoDefaultsOrNulls(models.Model):
406+    person = models.ForeignKey(Person)
407+    group = models.ForeignKey(Group)
408+    nodefaultnonull = models.CharField(max_length=5)
409+
410+__test__ = {'API_TESTS':"""
411+>>> from datetime import datetime
412+
413+### Creation and Saving Tests ###
414+
415+>>> bob = Person.objects.create(name = 'Bob')
416+>>> jim = Person.objects.create(name = 'Jim')
417+>>> jane = Person.objects.create(name = 'Jane')
418+>>> rock = Group.objects.create(name = 'Rock')
419+>>> roll = Group.objects.create(name = 'Roll')
420+
421+# We start out by making sure that the Group 'rock' has no members.
422+>>> rock.members.all()
423+[]
424+
425+# To make Jim a member of Group Rock, simply create a Membership object.
426+>>> m1 = Membership.objects.create(person = jim, group = rock)
427+
428+# We can do the same for Jane and Rock.
429+>>> m2 = Membership.objects.create(person = jane, group = rock)
430+
431+# Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
432+>>> rock.members.all()
433+[<Person: Jim>, <Person: Jane>]
434+
435+# Now we can add a bunch more Membership objects to test with.
436+>>> m3 = Membership.objects.create(person = bob, group = roll)
437+>>> m4 = Membership.objects.create(person = jim, group = roll)
438+>>> m5 = Membership.objects.create(person = jane, group = roll)
439+
440+# We can get Jim's Group membership as with any ForeignKey.
441+>>> jim.group_set.all()
442+[<Group: Rock>, <Group: Roll>]
443+
444+# Querying the intermediary model works like normal. 
445+# In this case we get Jane's membership to Rock.
446+>>> m = Membership.objects.get(person = jane, group = rock)
447+>>> m
448+<Membership: Jane is a member of Rock>
449+
450+# Now we set some date_joined dates for further testing.
451+>>> m2.invite_reason = "She was just awesome."
452+>>> m2.date_joined = datetime(2006, 1, 1)
453+>>> m2.save()
454+
455+>>> m5.date_joined = datetime(2004, 1, 1)
456+>>> m5.save()
457+
458+>>> m3.date_joined = datetime(2004, 1, 1)
459+>>> m3.save()
460+
461+# It's not only get that works.  Filter works like normal as well.
462+>>> Membership.objects.filter(person = jim)
463+[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
464+
465+
466+### Forward Descriptors Tests ###
467+
468+# Due to complications with adding via an intermediary model, the add method is
469+# not provided.
470+>>> rock.members.add(bob)
471+Traceback (most recent call last):
472+...
473+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
474+
475+# Remove has similar complications, and is not provided either.
476+>>> rock.members.remove(jim)
477+Traceback (most recent call last):
478+...
479+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
480+
481+# Here we back up the list of all members of Rock.
482+>>> backup = list(rock.members.all())
483+# ...and we verify that it has worked.
484+>>> backup
485+[<Person: Jim>, <Person: Jane>]
486+
487+# The clear function should still work.
488+>>> rock.members.clear()
489+# Now there will be no members of Rock.
490+>>> rock.members.all()
491+[]
492+
493+# Assignment should not work with models specifying a through model for many of
494+# the same reasons as adding.
495+>>> rock.members = backup
496+Traceback (most recent call last):
497+...
498+AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
499+
500+# Let's re-save those instances that we've cleared.
501+>>> m1.save()
502+>>> m2.save()
503+
504+# Verifying that those instances were re-saved successfully.
505+>>> rock.members.all()
506+[<Person: Jim>, <Person: Jane>]
507+
508+
509+### Reverse Descriptors Tests ###
510+
511+# Due to complications with adding via an intermediary model, the add method is
512+# not provided.
513+>>> bob.group_set.add(rock)
514+Traceback (most recent call last):
515+...
516+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
517+
518+# Remove has similar complications, and is not provided either.
519+>>> jim.group_set.remove(rock)
520+Traceback (most recent call last):
521+...
522+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
523+
524+# Here we back up the list of all of Jim's groups.
525+>>> backup = list(jim.group_set.all())
526+>>> backup
527+[<Group: Rock>, <Group: Roll>]
528+
529+# The clear function should still work.
530+>>> jim.group_set.clear()
531+# Now Jim will be in no groups.
532+>>> jim.group_set.all()
533+[]
534+
535+# Assignment should not work with models specifying a through model for many of
536+# the same reasons as adding.
537+>>> jim.group_set = backup
538+Traceback (most recent call last):
539+...
540+AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
541+
542+# Let's re-save those instances that we've cleared.
543+>>> m1.save()
544+>>> m4.save()
545+
546+# Verifying that those instances were re-saved successfully.
547+>>> jim.group_set.all()
548+[<Group: Rock>, <Group: Roll>]
549+
550+### Custom Tests ###
551+
552+# Let's see if we can query through our second relationship.
553+>>> rock.custom_members.all()
554+[]
555+# We can query in the opposite direction as well.
556+>>> bob.custom.all()
557+[]
558+
559+# Let's create some membership objects in this custom relationship.
560+>>> cm1 = CustomMembership.objects.create(person = bob, group = rock)
561+>>> cm2 = CustomMembership.objects.create(person = jim, group = rock)
562+
563+# If we get the number of people in Rock, it should be both Bob and Jim.
564+>>> rock.custom_members.all()
565+[<Person: Bob>, <Person: Jim>]
566+
567+# Bob should only be in one custom group.
568+>>> bob.custom.all()
569+[<Group: Rock>]
570+
571+# Let's make sure our new descriptors don't conflict with the FK related_name.
572+>>> bob.custom_person_related_name.all()
573+[<CustomMembership: Bob is a member of Rock>]
574+
575+### QUERY TESTS ###
576+
577+# We can query for the related model by using its attribute name (members, in
578+# this case).
579+>>> Group.objects.filter(members__name='Bob')
580+[<Group: Roll>]
581+
582+# To query through the intermediary model, we specify its model name.
583+# In this case, membership.
584+>>> Group.objects.filter(membership__invite_reason = "She was just awesome.")
585+[<Group: Rock>]
586+
587+# If we want to query in the reverse direction by the related model, use its
588+# model name (group, in this case).
589+>>> Person.objects.filter(group__name="Rock")
590+[<Person: Jim>, <Person: Jane>]
591+
592+# If the m2m field has specified a related_name, using that will work.
593+>>> Person.objects.filter(custom__name="Rock")
594+[<Person: Bob>, <Person: Jim>]
595+
596+# To query through the intermediary model in the reverse direction, we again
597+# specify its model name (membership, in this case).
598+>>> Person.objects.filter(membership__invite_reason = "She was just awesome.")
599+[<Person: Jane>]
600+
601+# Let's see all of the groups that Jane joined after 1 Jan 2005:
602+>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
603+... membership__person = jane)
604+[<Group: Rock>]
605+
606+# Queries also work in the reverse direction: Now let's see all of the people
607+# that have joined Rock since 1 Jan 2005:
608+>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
609+... membership__group = rock)
610+[<Person: Jim>, <Person: Jane>]
611+
612+# Conceivably, queries through membership could return correct, but non-unique
613+# querysets.  To demonstrate this, we query for all people who have joined a
614+# group after 2004:
615+>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1))
616+[<Person: Jim>, <Person: Jim>, <Person: Jane>]
617+
618+# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
619+>>> [(m.person.name, m.group.name) for m in
620+... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))]
621+[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
622+
623+# QuerySet's distinct() method can correct this problem.
624+>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct()
625+[<Person: Jim>, <Person: Jane>]
626+"""}
627\ No newline at end of file