Code

Ticket #6095: 6095-qsrf.diff

File 6095-qsrf.diff, 26.3 KB (added by floguy, 6 years ago)

Updated the patch to apply to queryset-refactor. Currently two of my tests which pass on Trunk do not pass on qs-rf, and I'm not exactly sure why. Will have to wait to see more documentation on qs-rf before I can resolve.

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