Code

Ticket #6095: 6095-nfadmin.3.diff

File 6095-nfadmin.3.diff, 26.7 KB (added by brosner, 6 years ago)

updated to r7500 of nfa

Line 
1diff --git a/AUTHORS b/AUTHORS
2index fe4755b..6622871 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/admin/options.py b/django/contrib/admin/options.py
14index 29ce10a..671278c 100644
15--- a/django/contrib/admin/options.py
16+++ b/django/contrib/admin/options.py
17@@ -174,7 +174,10 @@ class BaseModelAdmin(object):
18                 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
19             else:
20                 if isinstance(db_field, models.ManyToManyField):
21-                    if db_field.name in self.raw_id_fields:
22+                    # If it uses an intermediary model, don't show field in admin.
23+                    if db_field.rel.through != None:
24+                        return None
25+                    elif db_field.name in self.raw_id_fields:
26                         kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
27                         kwargs['help_text'] = ''
28                     elif db_field.name in (self.filter_vertical + self.filter_horizontal):
29diff --git a/django/core/management/sql.py b/django/core/management/sql.py
30index 1ccb100..c3279c1 100644
31--- a/django/core/management/sql.py
32+++ b/django/core/management/sql.py
33@@ -354,7 +354,7 @@ def many_to_many_sql_for_model(model, style):
34     qn = connection.ops.quote_name
35     inline_references = connection.features.inline_fk_references
36     for f in opts.local_many_to_many:
37-        if not isinstance(f.rel, generic.GenericRel):
38+        if not isinstance(f.rel, generic.GenericRel) and f.creates_table:
39             tablespace = f.db_tablespace or opts.db_tablespace
40             if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
41                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
42diff --git a/django/core/management/validation.py b/django/core/management/validation.py
43index 45dc899..693bf65 100644
44--- a/django/core/management/validation.py
45+++ b/django/core/management/validation.py
46@@ -102,6 +102,7 @@ def get_validation_errors(outfile, app=None):
47                         if r.get_accessor_name() == rel_query_name:
48                             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))
49 
50+        seen_intermediary_signatures = []
51         for i, f in enumerate(opts.local_many_to_many):
52             # Check to see if the related m2m field will clash with any
53             # existing fields, m2m fields, m2m related objects or related
54@@ -112,6 +113,28 @@ def get_validation_errors(outfile, app=None):
55                 # so skip the next section
56                 if isinstance(f.rel.to, (str, unicode)):
57                     continue
58+            if hasattr(f.rel, 'through') and f.rel.through != None:
59+                intermediary_model = None
60+                for model in models.get_models():
61+                    if model._meta.module_name == f.rel.through.lower():
62+                        intermediary_model = model
63+                if intermediary_model == None:
64+                    e.add(opts, "%s has a manually-defined m2m relationship through a model (%s) which does not exist." % (f.name, f.rel.through))
65+                else:
66+                    signature = (f.rel.to, cls, intermediary_model)
67+                    if signature in seen_intermediary_signatures:
68+                        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))
69+                    else:
70+                        seen_intermediary_signatures.append(signature)
71+                    seen_related_fk, seen_this_fk = False, False
72+                    for field in intermediary_model._meta.fields:
73+                        if field.rel:
74+                            if field.rel.to == f.rel.to:
75+                                seen_related_fk = True
76+                            elif field.rel.to == cls:
77+                                seen_this_fk = True
78+                    if not seen_related_fk or not seen_this_fk:
79+                        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))
80 
81             rel_opts = f.rel.to._meta
82             rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
83diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
84index 0278081..ec2ca03 100644
85--- a/django/db/models/fields/related.py
86+++ b/django/db/models/fields/related.py
87@@ -310,7 +310,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@@ -324,6 +324,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@@ -333,15 +334,13 @@ def create_many_related_manager(superclass):
105 
106         def add(self, *objs):
107             self._add_items(self.source_col_name, self.target_col_name, *objs)
108-
109             # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
110             if self.symmetrical:
111                 self._add_items(self.target_col_name, self.source_col_name, *objs)
112         add.alters_data = True
113-
114+   
115         def remove(self, *objs):
116             self._remove_items(self.source_col_name, self.target_col_name, *objs)
117-
118             # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
119             if self.symmetrical:
120                 self._remove_items(self.target_col_name, self.source_col_name, *objs)
121@@ -356,6 +355,8 @@ def create_many_related_manager(superclass):
122         clear.alters_data = True
123 
124         def create(self, **kwargs):
125+            if through:
126+                raise AttributeError, "'ManyRelatedManager' object has no attribute 'create'"
127             new_obj = self.model(**kwargs)
128             new_obj.save()
129             self.add(new_obj)
130@@ -423,6 +424,12 @@ def create_many_related_manager(superclass):
131                 [self._pk_val])
132             transaction.commit_unless_managed()
133 
134+    # If it's an intermediary model, detach the add, remove, and create methods
135+    # from this ManyRelatedManager.
136+    if through:
137+        del ManyRelatedManager.add
138+        del ManyRelatedManager.remove
139+
140     return ManyRelatedManager
141 
142 class ManyRelatedObjectsDescriptor(object):
143@@ -443,7 +450,7 @@ class ManyRelatedObjectsDescriptor(object):
144         # model's default manager.
145         rel_model = self.related.model
146         superclass = rel_model._default_manager.__class__
147-        RelatedManager = create_many_related_manager(superclass)
148+        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
149 
150         qn = connection.ops.quote_name
151         manager = RelatedManager(
152@@ -462,6 +469,10 @@ class ManyRelatedObjectsDescriptor(object):
153         if instance is None:
154             raise AttributeError, "Manager must be accessed via instance"
155 
156+        through = getattr(self.related.field.rel, 'through', None)
157+        if through:
158+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
159+       
160         manager = self.__get__(instance)
161         manager.clear()
162         manager.add(*value)
163@@ -484,7 +495,7 @@ class ReverseManyRelatedObjectsDescriptor(object):
164         # model's default manager.
165         rel_model=self.field.rel.to
166         superclass = rel_model._default_manager.__class__
167-        RelatedManager = create_many_related_manager(superclass)
168+        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
169 
170         qn = connection.ops.quote_name
171         manager = RelatedManager(
172@@ -503,6 +514,10 @@ class ReverseManyRelatedObjectsDescriptor(object):
173         if instance is None:
174             raise AttributeError, "Manager must be accessed via instance"
175 
176+        through = getattr(self.field.rel, 'through', None)
177+        if through:
178+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
179+
180         manager = self.__get__(instance)
181         manager.clear()
182         manager.add(*value)
183@@ -554,7 +569,7 @@ class OneToOneRel(ManyToOneRel):
184 
185 class ManyToManyRel(object):
186     def __init__(self, to, num_in_admin=0, related_name=None,
187-        limit_choices_to=None, symmetrical=True):
188+        limit_choices_to=None, symmetrical=True, through=None):
189         self.to = to
190         self.num_in_admin = num_in_admin
191         self.related_name = related_name
192@@ -564,6 +579,7 @@ class ManyToManyRel(object):
193         self.edit_inline = False
194         self.symmetrical = symmetrical
195         self.multiple = True
196+        self.through = through
197 
198 class ForeignKey(RelatedField, Field):
199     empty_strings_allowed = False
200@@ -695,8 +711,14 @@ class ManyToManyField(RelatedField, Field):
201             num_in_admin=kwargs.pop('num_in_admin', 0),
202             related_name=kwargs.pop('related_name', None),
203             limit_choices_to=kwargs.pop('limit_choices_to', None),
204-            symmetrical=kwargs.pop('symmetrical', True))
205+            symmetrical=kwargs.pop('symmetrical', True),
206+            through=kwargs.pop('through', None))
207         self.db_table = kwargs.pop('db_table', None)
208+        if kwargs['rel'].through:
209+            self.creates_table = False
210+            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used."
211+        else:
212+            self.creates_table = True
213         Field.__init__(self, **kwargs)
214 
215         msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
216@@ -711,7 +733,9 @@ class ManyToManyField(RelatedField, Field):
217 
218     def _get_m2m_db_table(self, opts):
219         "Function that can be curried to provide the m2m table name for this relation"
220-        if self.db_table:
221+        if self.rel.through != None:
222+            return get_model(opts.app_label, self.rel.through)._meta.db_table
223+        elif self.db_table:
224             return self.db_table
225         else:
226             return '%s_%s' % (opts.db_table, self.name)
227@@ -719,7 +743,11 @@ class ManyToManyField(RelatedField, Field):
228     def _get_m2m_column_name(self, related):
229         "Function that can be curried to provide the source column name for the m2m table"
230         # If this is an m2m relation to self, avoid the inevitable name clash
231-        if related.model == related.parent_model:
232+        if self.rel.through != None:
233+            field = related.model._meta.get_related_object(self.rel.through).field
234+            attname, column = field.get_attname_column()
235+            return column
236+        elif related.model == related.parent_model:
237             return 'from_' + related.model._meta.object_name.lower() + '_id'
238         else:
239             return related.model._meta.object_name.lower() + '_id'
240@@ -727,7 +755,11 @@ class ManyToManyField(RelatedField, Field):
241     def _get_m2m_reverse_name(self, related):
242         "Function that can be curried to provide the related column name for the m2m table"
243         # If this is an m2m relation to self, avoid the inevitable name clash
244-        if related.model == related.parent_model:
245+        if self.rel.through != None:
246+            field = related.parent_model._meta.get_related_object(self.rel.through).field
247+            attname, column = field.get_attname_column()
248+            return column
249+        elif related.model == related.parent_model:
250             return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
251         else:
252             return related.parent_model._meta.object_name.lower() + '_id'
253diff --git a/django/db/models/options.py b/django/db/models/options.py
254index 3948a5f..02c75b1 100644
255--- a/django/db/models/options.py
256+++ b/django/db/models/options.py
257@@ -10,7 +10,7 @@ from django.db.models.related import RelatedObject
258 from django.db.models.fields.related import ManyToManyRel
259 from django.db.models.fields import AutoField, FieldDoesNotExist
260 from django.db.models.fields.proxy import OrderWrt
261-from django.db.models.loading import get_models, app_cache_ready
262+from django.db.models.loading import get_models, get_model, app_cache_ready
263 from django.utils.translation import activate, deactivate_all, get_language, string_concat
264 from django.utils.encoding import force_unicode, smart_str
265 from django.utils.datastructures import SortedDict
266@@ -373,6 +373,15 @@ class Options(object):
267             follow = self.get_follow()
268         return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
269 
270+    def get_related_object(self, from_model):
271+        "Gets the RelatedObject which links from from_model to this model."
272+        if isinstance(from_model, str):
273+            from_model = get_model(self.app_label, from_model)
274+        for related_object in self.get_all_related_objects():
275+            if related_object.model == from_model:
276+                return related_object
277+        return None
278+
279     def get_data_holders(self, follow=None):
280         if follow == None:
281             follow = self.get_follow()
282diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
283index 48e574a..ba74c4a 100644
284--- a/tests/modeltests/invalid_models/models.py
285+++ b/tests/modeltests/invalid_models/models.py
286@@ -110,6 +110,31 @@ class Car(models.Model):
287 class MissingRelations(models.Model):
288     rel1 = models.ForeignKey("Rel1")
289     rel2 = models.ManyToManyField("Rel2")
290+   
291+class MissingManualM2MModel(models.Model):
292+    name = models.CharField(max_length=5)
293+    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
294+   
295+class Person(models.Model):
296+    name = models.CharField(max_length=5)
297+
298+class Group(models.Model):
299+    name = models.CharField(max_length=5)
300+    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
301+    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
302+
303+class GroupTwo(models.Model):
304+    name = models.CharField(max_length=5)
305+    primary = models.ManyToManyField(Person, through="Membership")
306+    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
307+
308+class Membership(models.Model):
309+    person = models.ForeignKey(Person)
310+    group = models.ForeignKey(Group)
311+    not_default_or_null = models.CharField(max_length=5)
312+
313+class MembershipMissingFK(models.Model):
314+    person = models.ForeignKey(Person)
315 
316 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
317 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
318@@ -195,4 +220,8 @@ invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi
319 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'.
320 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
321 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
322+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.
323+invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist.
324+invalid_models.grouptwo: primary has a manually-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo
325+invalid_models.grouptwo: secondary has a manually-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo
326 """
327diff --git a/tests/modeltests/m2m_manual/__init__.py b/tests/modeltests/m2m_manual/__init__.py
328new file mode 100644
329index 0000000..8b13789
330--- /dev/null
331+++ b/tests/modeltests/m2m_manual/__init__.py
332@@ -0,0 +1 @@
333+
334diff --git a/tests/modeltests/m2m_manual/models.py b/tests/modeltests/m2m_manual/models.py
335new file mode 100644
336index 0000000..01764e3
337--- /dev/null
338+++ b/tests/modeltests/m2m_manual/models.py
339@@ -0,0 +1,278 @@
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+>>> rock.members.create(name = 'Anne')
451+Traceback (most recent call last):
452+...
453+AttributeError: 'ManyRelatedManager' object has no attribute 'create'
454+
455+# Remove has similar complications, and is not provided either.
456+>>> rock.members.remove(jim)
457+Traceback (most recent call last):
458+...
459+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
460+
461+# Here we back up the list of all members of Rock.
462+>>> backup = list(rock.members.all())
463+
464+# ...and we verify that it has worked.
465+>>> backup
466+[<Person: Jim>, <Person: Jane>]
467+
468+# The clear function should still work.
469+>>> rock.members.clear()
470+
471+# Now there will be no members of Rock.
472+>>> rock.members.all()
473+[]
474+
475+# Assignment should not work with models specifying a through model for many of
476+# the same reasons as adding.
477+>>> rock.members = backup
478+Traceback (most recent call last):
479+...
480+AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
481+
482+# Let's re-save those instances that we've cleared.
483+>>> m1.save()
484+>>> m2.save()
485+
486+# Verifying that those instances were re-saved successfully.
487+>>> rock.members.all()
488+[<Person: Jim>, <Person: Jane>]
489+
490+
491+### Reverse Descriptors Tests ###
492+
493+# Due to complications with adding via an intermediary model, the add method is
494+# not provided.
495+>>> bob.group_set.add(rock)
496+Traceback (most recent call last):
497+...
498+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
499+
500+# Create is another method that should not work correctly, as it suffers from
501+# the same problems as add.
502+>>> bob.group_set.create(name = 'Funk')
503+Traceback (most recent call last):
504+...
505+AttributeError: 'ManyRelatedManager' object has no attribute 'create'
506+
507+# Remove has similar complications, and is not provided either.
508+>>> jim.group_set.remove(rock)
509+Traceback (most recent call last):
510+...
511+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
512+
513+# Here we back up the list of all of Jim's groups.
514+>>> backup = list(jim.group_set.all())
515+>>> backup
516+[<Group: Rock>, <Group: Roll>]
517+
518+# The clear function should still work.
519+>>> jim.group_set.clear()
520+
521+# Now Jim will be in no groups.
522+>>> jim.group_set.all()
523+[]
524+
525+# Assignment should not work with models specifying a through model for many of
526+# the same reasons as adding.
527+>>> jim.group_set = backup
528+Traceback (most recent call last):
529+...
530+AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
531+
532+# Let's re-save those instances that we've cleared.
533+>>> m1.save()
534+>>> m4.save()
535+
536+# Verifying that those instances were re-saved successfully.
537+>>> jim.group_set.all()
538+[<Group: Rock>, <Group: Roll>]
539+
540+### Custom Tests ###
541+
542+# Let's see if we can query through our second relationship.
543+>>> rock.custom_members.all()
544+[]
545+
546+# We can query in the opposite direction as well.
547+>>> bob.custom.all()
548+[]
549+
550+# Let's create some membership objects in this custom relationship.
551+>>> cm1 = CustomMembership.objects.create(person = bob, group = rock)
552+>>> cm2 = CustomMembership.objects.create(person = jim, group = rock)
553+
554+# If we get the number of people in Rock, it should be both Bob and Jim.
555+>>> rock.custom_members.all()
556+[<Person: Bob>, <Person: Jim>]
557+
558+# Bob should only be in one custom group.
559+>>> bob.custom.all()
560+[<Group: Rock>]
561+
562+# Let's make sure our new descriptors don't conflict with the FK related_name.
563+>>> bob.custom_person_related_name.all()
564+[<CustomMembership: Bob is a member of Rock>]
565+
566+### QUERY TESTS ###
567+
568+# We can query for the related model by using its attribute name (members, in
569+# this case).
570+>>> Group.objects.filter(members__name='Bob')
571+[<Group: Roll>]
572+
573+# To query through the intermediary model, we specify its model name.
574+# In this case, membership.
575+>>> Group.objects.filter(membership__invite_reason = "She was just awesome.")
576+[<Group: Rock>]
577+
578+# If we want to query in the reverse direction by the related model, use its
579+# model name (group, in this case).
580+>>> Person.objects.filter(group__name="Rock")
581+[<Person: Jim>, <Person: Jane>]
582+
583+# If the m2m field has specified a related_name, using that will work.
584+>>> Person.objects.filter(custom__name="Rock")
585+[<Person: Bob>, <Person: Jim>]
586+
587+# To query through the intermediary model in the reverse direction, we again
588+# specify its model name (membership, in this case).
589+>>> Person.objects.filter(membership__invite_reason = "She was just awesome.")
590+[<Person: Jane>]
591+
592+# Let's see all of the groups that Jane joined after 1 Jan 2005:
593+>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
594+... membership__person = jane)
595+[<Group: Rock>]
596+
597+# Queries also work in the reverse direction: Now let's see all of the people
598+# that have joined Rock since 1 Jan 2005:
599+>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
600+... membership__group = rock)
601+[<Person: Jim>, <Person: Jane>]
602+
603+# Conceivably, queries through membership could return correct, but non-unique
604+# querysets.  To demonstrate this, we query for all people who have joined a
605+# group after 2004:
606+>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1))
607+[<Person: Jim>, <Person: Jim>, <Person: Jane>]
608+
609+# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
610+>>> [(m.person.name, m.group.name) for m in
611+... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))]
612+[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
613+
614+# QuerySet's distinct() method can correct this problem.
615+>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct()
616+[<Person: Jim>, <Person: Jane>]
617+"""}
618\ No newline at end of file