Code

Ticket #6095: 6095-nfadmin.2.diff

File 6095-nfadmin.2.diff, 25.9 KB (added by floguy, 6 years ago)

Made sure that create is not allowed. This was an oversight in the original patch, but now there are tests which verify that create should not be allowed on a ManyRelatedManager. Also removed new.instancemethod craziness because it was not necessary. All tests pass.

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