Code

Ticket #6095: 6095-r8090.diff

File 6095-r8090.diff, 39.5 KB (added by russellm, 6 years ago)

Revised patch, with cleaned up internals (especially w.r.t field lookup)

Line 
1Index: django/db/models/fields/related.py
2===================================================================
3--- django/db/models/fields/related.py  (revision 8090)
4+++ django/db/models/fields/related.py  (working copy)
5@@ -23,7 +23,7 @@
6 
7 pending_lookups = {}
8 
9-def add_lazy_relation(cls, field, relation):
10+def add_lazy_relation(cls, field, relation, operation):
11     """
12     Adds a lookup on ``cls`` when a related field is defined using a string,
13     i.e.::
14@@ -45,6 +45,8 @@
15     If the other model hasn't yet been loaded -- almost a given if you're using
16     lazy relationships -- then the relation won't be set up until the
17     class_prepared signal fires at the end of model initialization.
18+   
19+    operation is the work that must be performed once the relation can be resolved.
20     """
21     # Check for recursive relations
22     if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
23@@ -66,11 +68,10 @@
24     # is prepared.
25     model = get_model(app_label, model_name, False)
26     if model:
27-        field.rel.to = model
28-        field.do_related_class(model, cls)
29+        operation(field, model, cls)
30     else:
31         key = (app_label, model_name)
32-        value = (cls, field)
33+        value = (cls, field, operation)
34         pending_lookups.setdefault(key, []).append(value)
35 
36 def do_pending_lookups(sender):
37@@ -78,9 +79,8 @@
38     Handle any pending relations to the sending model. Sent from class_prepared.
39     """
40     key = (sender._meta.app_label, sender.__name__)
41-    for cls, field in pending_lookups.pop(key, []):
42-        field.rel.to = sender
43-        field.do_related_class(sender, cls)
44+    for cls, field, operation in pending_lookups.pop(key, []):
45+        operation(field, sender, cls)
46 
47 dispatcher.connect(do_pending_lookups, signal=signals.class_prepared)
48 
49@@ -108,7 +108,10 @@
50 
51         other = self.rel.to
52         if isinstance(other, basestring):
53-            add_lazy_relation(cls, self, other)
54+            def resolve_related_class(field, model, cls):
55+                field.rel.to = model
56+                field.do_related_class(model, cls)
57+            add_lazy_relation(cls, self, other, resolve_related_class)
58         else:
59             self.do_related_class(other, cls)
60 
61@@ -339,7 +342,7 @@
62             manager.clear()
63         manager.add(*value)
64 
65-def create_many_related_manager(superclass):
66+def create_many_related_manager(superclass, through=False):
67     """Creates a manager that subclasses 'superclass' (which is a Manager)
68     and adds behavior for many-to-many related objects."""
69     class ManyRelatedManager(superclass):
70@@ -353,6 +356,7 @@
71             self.join_table = join_table
72             self.source_col_name = source_col_name
73             self.target_col_name = target_col_name
74+            self.through = through
75             self._pk_val = self.instance._get_pk_val()
76             if self._pk_val is None:
77                 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
78@@ -360,21 +364,24 @@
79         def get_query_set(self):
80             return superclass.get_query_set(self).filter(**(self.core_filters))
81 
82-        def add(self, *objs):
83-            self._add_items(self.source_col_name, self.target_col_name, *objs)
84+        # If the ManyToMany relation has an intermediary model,
85+        # the add and remove methods do not exist.
86+        if through is None:
87+            def add(self, *objs):
88+                self._add_items(self.source_col_name, self.target_col_name, *objs)
89 
90-            # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
91-            if self.symmetrical:
92-                self._add_items(self.target_col_name, self.source_col_name, *objs)
93-        add.alters_data = True
94+                # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
95+                if self.symmetrical:
96+                    self._add_items(self.target_col_name, self.source_col_name, *objs)
97+            add.alters_data = True
98 
99-        def remove(self, *objs):
100-            self._remove_items(self.source_col_name, self.target_col_name, *objs)
101+            def remove(self, *objs):
102+                self._remove_items(self.source_col_name, self.target_col_name, *objs)
103 
104-            # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
105-            if self.symmetrical:
106-                self._remove_items(self.target_col_name, self.source_col_name, *objs)
107-        remove.alters_data = True
108+                # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
109+                if self.symmetrical:
110+                    self._remove_items(self.target_col_name, self.source_col_name, *objs)
111+            remove.alters_data = True
112 
113         def clear(self):
114             self._clear_items(self.source_col_name)
115@@ -385,6 +392,10 @@
116         clear.alters_data = True
117 
118         def create(self, **kwargs):
119+            # This check needs to be done here, since we can't later remove this
120+            # from the method lookup table, as we do with add and remove.
121+            if through is not None:
122+                raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
123             new_obj = self.model(**kwargs)
124             new_obj.save()
125             self.add(new_obj)
126@@ -472,7 +483,7 @@
127         # model's default manager.
128         rel_model = self.related.model
129         superclass = rel_model._default_manager.__class__
130-        RelatedManager = create_many_related_manager(superclass)
131+        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
132 
133         qn = connection.ops.quote_name
134         manager = RelatedManager(
135@@ -491,6 +502,10 @@
136         if instance is None:
137             raise AttributeError, "Manager must be accessed via instance"
138 
139+        through = getattr(self.related.field.rel, 'through', None)
140+        if through is not None:
141+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
142+
143         manager = self.__get__(instance)
144         manager.clear()
145         manager.add(*value)
146@@ -513,7 +528,7 @@
147         # model's default manager.
148         rel_model=self.field.rel.to
149         superclass = rel_model._default_manager.__class__
150-        RelatedManager = create_many_related_manager(superclass)
151+        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
152 
153         qn = connection.ops.quote_name
154         manager = RelatedManager(
155@@ -532,6 +547,10 @@
156         if instance is None:
157             raise AttributeError, "Manager must be accessed via instance"
158 
159+        through = getattr(self.field.rel, 'through', None)
160+        if through is not None:
161+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through
162+
163         manager = self.__get__(instance)
164         manager.clear()
165         manager.add(*value)
166@@ -583,7 +602,7 @@
167 
168 class ManyToManyRel(object):
169     def __init__(self, to, num_in_admin=0, related_name=None,
170-        limit_choices_to=None, symmetrical=True):
171+        limit_choices_to=None, symmetrical=True, through=None):
172         self.to = to
173         self.num_in_admin = num_in_admin
174         self.related_name = related_name
175@@ -593,7 +612,9 @@
176         self.edit_inline = False
177         self.symmetrical = symmetrical
178         self.multiple = True
179+        self.through = through
180 
181+
182 class ForeignKey(RelatedField, Field):
183     empty_strings_allowed = False
184     def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
185@@ -722,8 +743,16 @@
186             num_in_admin=kwargs.pop('num_in_admin', 0),
187             related_name=kwargs.pop('related_name', None),
188             limit_choices_to=kwargs.pop('limit_choices_to', None),
189-            symmetrical=kwargs.pop('symmetrical', True))
190+            symmetrical=kwargs.pop('symmetrical', True),
191+            through=kwargs.pop('through', None))
192+           
193         self.db_table = kwargs.pop('db_table', None)
194+        if kwargs['rel'].through is not None:
195+            self.creates_table = False
196+            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
197+        else:
198+            self.creates_table = True
199+
200         Field.__init__(self, **kwargs)
201 
202         msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
203@@ -738,23 +767,44 @@
204 
205     def _get_m2m_db_table(self, opts):
206         "Function that can be curried to provide the m2m table name for this relation"
207-        if self.db_table:
208+        if self.rel.through is not None:
209+            return self.rel.through_model._meta.db_table
210+        elif self.db_table:
211             return self.db_table
212         else:
213             return '%s_%s' % (opts.db_table, self.name)
214 
215     def _get_m2m_column_name(self, related):
216         "Function that can be curried to provide the source column name for the m2m table"
217+        if self.rel.through is not None:
218+            for f in self.rel.through_model._meta.fields:
219+                if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
220+                    return f.column
221         # If this is an m2m relation to self, avoid the inevitable name clash
222-        if related.model == related.parent_model:
223+        elif related.model == related.parent_model:
224             return 'from_' + related.model._meta.object_name.lower() + '_id'
225         else:
226             return related.model._meta.object_name.lower() + '_id'
227 
228     def _get_m2m_reverse_name(self, related):
229         "Function that can be curried to provide the related column name for the m2m table"
230+        if self.rel.through is not None:
231+            found = False
232+            for f in self.rel.through_model._meta.fields:
233+                if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
234+                    if related.model == related.parent_model:
235+                        # If this is an m2m-intermediate to self,
236+                        # the first foreign key you find will be
237+                        # the source column. Keep searching for
238+                        # the second foreign key.
239+                        if found:
240+                            return f.column
241+                        else:
242+                            found = True
243+                    else:
244+                        return f.column
245         # If this is an m2m relation to self, avoid the inevitable name clash
246-        if related.model == related.parent_model:
247+        elif related.model == related.parent_model:
248             return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
249         else:
250             return related.parent_model._meta.object_name.lower() + '_id'
251@@ -797,7 +847,17 @@
252 
253         # Set up the accessor for the m2m table name for the relation
254         self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
255-
256+       
257+        # Populate some necessary rel arguments so that cross-app relations
258+        # work correctly.
259+        if isinstance(self.rel.through, basestring):
260+            def resolve_through_model(field, model, cls):
261+                field.rel.through_model = model
262+            add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
263+        elif self.rel.through:
264+            self.rel.through_model = self.rel.through
265+            self.rel.through = self.rel.through._meta.object_name
266+           
267         if isinstance(self.rel.to, basestring):
268             target = self.rel.to
269         else:
270Index: django/core/management/validation.py
271===================================================================
272--- django/core/management/validation.py        (revision 8090)
273+++ django/core/management/validation.py        (working copy)
274@@ -102,6 +102,7 @@
275                         if r.get_accessor_name() == rel_query_name:
276                             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))
277 
278+        seen_intermediary_signatures = []
279         for i, f in enumerate(opts.local_many_to_many):
280             # Check to see if the related m2m field will clash with any
281             # existing fields, m2m fields, m2m related objects or related
282@@ -112,7 +113,28 @@
283                 # so skip the next section
284                 if isinstance(f.rel.to, (str, unicode)):
285                     continue
286-
287+            if getattr(f.rel, 'through', None) is not None:
288+                if hasattr(f.rel, 'through_model'):
289+                    intermediary_model = f.rel.through_model
290+                    if intermediary_model not in models.get_models():
291+                        e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
292+                    signature = (f.rel.to, cls, intermediary_model)
293+                    if signature in seen_intermediary_signatures:
294+                        e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted.  Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, intermediary_model._meta.object_name))
295+                    else:
296+                        seen_intermediary_signatures.append(signature)
297+                    seen_related_fk, seen_this_fk = False, False
298+                    for field in intermediary_model._meta.fields:
299+                        if field.rel:
300+                            if not seen_related_fk and field.rel.to == f.rel.to:
301+                                seen_related_fk = True
302+                            elif field.rel.to == cls:
303+                                seen_this_fk = True
304+                    if not seen_related_fk or not seen_this_fk:
305+                        e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name))
306+                else:
307+                    e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
308+           
309             rel_opts = f.rel.to._meta
310             rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
311             rel_query_name = f.related_query_name()
312Index: django/core/management/sql.py
313===================================================================
314--- django/core/management/sql.py       (revision 8090)
315+++ django/core/management/sql.py       (working copy)
316@@ -353,7 +353,7 @@
317     qn = connection.ops.quote_name
318     inline_references = connection.features.inline_fk_references
319     for f in opts.local_many_to_many:
320-        if not isinstance(f.rel, generic.GenericRel):
321+        if f.creates_table:
322             tablespace = f.db_tablespace or opts.db_tablespace
323             if tablespace and connection.features.supports_tablespaces:
324                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
325Index: django/contrib/contenttypes/generic.py
326===================================================================
327--- django/contrib/contenttypes/generic.py      (revision 8090)
328+++ django/contrib/contenttypes/generic.py      (working copy)
329@@ -104,6 +104,9 @@
330                             limit_choices_to=kwargs.pop('limit_choices_to', None),
331                             symmetrical=kwargs.pop('symmetrical', True))
332 
333+        # By its very nature, a GenericRelation doesn't create a table.
334+        self.creates_table = False
335+
336         # Override content-type/object-id field names on the related class
337         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
338         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
339Index: tests/modeltests/invalid_models/models.py
340===================================================================
341--- tests/modeltests/invalid_models/models.py   (revision 8090)
342+++ tests/modeltests/invalid_models/models.py   (working copy)
343@@ -110,7 +110,41 @@
344 class MissingRelations(models.Model):
345     rel1 = models.ForeignKey("Rel1")
346     rel2 = models.ManyToManyField("Rel2")
347+   
348+class MissingManualM2MModel(models.Model):
349+    name = models.CharField(max_length=5)
350+    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
351+   
352+class Person(models.Model):
353+    name = models.CharField(max_length=5)
354 
355+class Group(models.Model):
356+    name = models.CharField(max_length=5)
357+    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
358+    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
359+
360+class GroupTwo(models.Model):
361+    name = models.CharField(max_length=5)
362+    primary = models.ManyToManyField(Person, through="Membership")
363+    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
364+
365+class Membership(models.Model):
366+    person = models.ForeignKey(Person)
367+    group = models.ForeignKey(Group)
368+    not_default_or_null = models.CharField(max_length=5)
369+
370+class MembershipMissingFK(models.Model):
371+    person = models.ForeignKey(Person)
372+
373+class PersonSelfRefM2M(models.Model):
374+    name = models.CharField(max_length=5)
375+    friends = models.ManyToManyField('self', through="Relationship")
376+
377+class Relationship(models.Model):
378+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
379+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
380+    date_added = models.DateTimeField()
381+
382 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
383 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
384 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
385@@ -195,4 +229,8 @@
386 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'.
387 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
388 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
389+invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted.  Please consider using an extra field on your intermediary model instead.
390+invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
391+invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
392+invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed
393 """
394Index: tests/modeltests/m2m_through/__init__.py
395===================================================================
396--- tests/modeltests/m2m_through/__init__.py    (revision 0)
397+++ tests/modeltests/m2m_through/__init__.py    (revision 0)
398@@ -0,0 +1 @@
399+
400Index: tests/modeltests/m2m_through/models.py
401===================================================================
402--- tests/modeltests/m2m_through/models.py      (revision 0)
403+++ tests/modeltests/m2m_through/models.py      (revision 0)
404@@ -0,0 +1,328 @@
405+from django.db import models
406+from datetime import datetime
407+
408+# M2M described on one of the models
409+class Person(models.Model):
410+    name = models.CharField(max_length=128)
411+
412+    def __unicode__(self):
413+        return self.name
414+
415+class Group(models.Model):
416+    name = models.CharField(max_length=128)
417+    members = models.ManyToManyField(Person, through='Membership')
418+    custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom")
419+    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
420+   
421+    def __unicode__(self):
422+        return self.name
423+
424+class Membership(models.Model):
425+    person = models.ForeignKey(Person)
426+    group = models.ForeignKey(Group)
427+    date_joined = models.DateTimeField(default=datetime.now)
428+    invite_reason = models.CharField(max_length=64, null=True)
429+   
430+    def __unicode__(self):
431+        return "%s is a member of %s" % (self.person.name, self.group.name)
432+
433+class CustomMembership(models.Model):
434+    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
435+    group = models.ForeignKey(Group)
436+    weird_fk = models.ForeignKey(Membership, null=True)
437+    date_joined = models.DateTimeField(default=datetime.now)
438+   
439+    def __unicode__(self):
440+        return "%s is a member of %s" % (self.person.name, self.group.name)
441+   
442+    class Meta:
443+        db_table = "test_table"
444+
445+class TestNoDefaultsOrNulls(models.Model):
446+    person = models.ForeignKey(Person)
447+    group = models.ForeignKey(Group)
448+    nodefaultnonull = models.CharField(max_length=5)
449+
450+class PersonSelfRefM2M(models.Model):
451+    name = models.CharField(max_length=5)
452+    friends = models.ManyToManyField('self', through="Friendship")
453+   
454+    def __unicode__(self):
455+        return self.name
456+
457+class Friendship(models.Model):
458+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
459+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
460+    date_friended = models.DateTimeField()
461+
462+__test__ = {'API_TESTS':"""
463+>>> from datetime import datetime
464+
465+### Creation and Saving Tests ###
466+
467+>>> bob = Person.objects.create(name='Bob')
468+>>> jim = Person.objects.create(name='Jim')
469+>>> jane = Person.objects.create(name='Jane')
470+>>> rock = Group.objects.create(name='Rock')
471+>>> roll = Group.objects.create(name='Roll')
472+
473+# We start out by making sure that the Group 'rock' has no members.
474+>>> rock.members.all()
475+[]
476+
477+# To make Jim a member of Group Rock, simply create a Membership object.
478+>>> m1 = Membership.objects.create(person=jim, group=rock)
479+
480+# We can do the same for Jane and Rock.
481+>>> m2 = Membership.objects.create(person=jane, group=rock)
482+
483+# Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
484+>>> rock.members.all()
485+[<Person: Jim>, <Person: Jane>]
486+
487+# Now we can add a bunch more Membership objects to test with.
488+>>> m3 = Membership.objects.create(person=bob, group=roll)
489+>>> m4 = Membership.objects.create(person=jim, group=roll)
490+>>> m5 = Membership.objects.create(person=jane, group=roll)
491+
492+# We can get Jim's Group membership as with any ForeignKey.
493+>>> jim.group_set.all()
494+[<Group: Rock>, <Group: Roll>]
495+
496+# Querying the intermediary model works like normal. 
497+# In this case we get Jane's membership to Rock.
498+>>> m = Membership.objects.get(person=jane, group=rock)
499+>>> m
500+<Membership: Jane is a member of Rock>
501+
502+# Now we set some date_joined dates for further testing.
503+>>> m2.invite_reason = "She was just awesome."
504+>>> m2.date_joined = datetime(2006, 1, 1)
505+>>> m2.save()
506+
507+>>> m5.date_joined = datetime(2004, 1, 1)
508+>>> m5.save()
509+
510+>>> m3.date_joined = datetime(2004, 1, 1)
511+>>> m3.save()
512+
513+# It's not only get that works. Filter works like normal as well.
514+>>> Membership.objects.filter(person=jim)
515+[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
516+
517+
518+### Forward Descriptors Tests ###
519+
520+# Due to complications with adding via an intermediary model,
521+# the add method is not provided.
522+>>> rock.members.add(bob)
523+Traceback (most recent call last):
524+...
525+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
526+
527+# Create is also disabled as it suffers from the same problems as add.
528+>>> rock.members.create(name='Anne')
529+Traceback (most recent call last):
530+...
531+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
532+
533+# Remove has similar complications, and is not provided either.
534+>>> rock.members.remove(jim)
535+Traceback (most recent call last):
536+...
537+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
538+
539+# Here we back up the list of all members of Rock.
540+>>> backup = list(rock.members.all())
541+
542+# ...and we verify that it has worked.
543+>>> backup
544+[<Person: Jim>, <Person: Jane>]
545+
546+# The clear function should still work.
547+>>> rock.members.clear()
548+
549+# Now there will be no members of Rock.
550+>>> rock.members.all()
551+[]
552+
553+# Assignment should not work with models specifying a through model for many of
554+# the same reasons as adding.
555+>>> rock.members = backup
556+Traceback (most recent call last):
557+...
558+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
559+
560+# Let's re-save those instances that we've cleared.
561+>>> m1.save()
562+>>> m2.save()
563+
564+# Verifying that those instances were re-saved successfully.
565+>>> rock.members.all()
566+[<Person: Jim>, <Person: Jane>]
567+
568+
569+### Reverse Descriptors Tests ###
570+
571+# Due to complications with adding via an intermediary model,
572+# the add method is not provided.
573+>>> bob.group_set.add(rock)
574+Traceback (most recent call last):
575+...
576+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
577+
578+# Create is also disabled as it suffers from the same problems as add.
579+>>> bob.group_set.create(name='Funk')
580+Traceback (most recent call last):
581+...
582+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
583+
584+# Remove has similar complications, and is not provided either.
585+>>> jim.group_set.remove(rock)
586+Traceback (most recent call last):
587+...
588+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
589+
590+# Here we back up the list of all of Jim's groups.
591+>>> backup = list(jim.group_set.all())
592+>>> backup
593+[<Group: Rock>, <Group: Roll>]
594+
595+# The clear function should still work.
596+>>> jim.group_set.clear()
597+
598+# Now Jim will be in no groups.
599+>>> jim.group_set.all()
600+[]
601+
602+# Assignment should not work with models specifying a through model for many of
603+# the same reasons as adding.
604+>>> jim.group_set = backup
605+Traceback (most recent call last):
606+...
607+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
608+
609+# Let's re-save those instances that we've cleared.
610+>>> m1.save()
611+>>> m4.save()
612+
613+# Verifying that those instances were re-saved successfully.
614+>>> jim.group_set.all()
615+[<Group: Rock>, <Group: Roll>]
616+
617+### Custom Tests ###
618+
619+# Let's see if we can query through our second relationship.
620+>>> rock.custom_members.all()
621+[]
622+
623+# We can query in the opposite direction as well.
624+>>> bob.custom.all()
625+[]
626+
627+# Let's create some membership objects in this custom relationship.
628+>>> cm1 = CustomMembership.objects.create(person=bob, group=rock)
629+>>> cm2 = CustomMembership.objects.create(person=jim, group=rock)
630+
631+# If we get the number of people in Rock, it should be both Bob and Jim.
632+>>> rock.custom_members.all()
633+[<Person: Bob>, <Person: Jim>]
634+
635+# Bob should only be in one custom group.
636+>>> bob.custom.all()
637+[<Group: Rock>]
638+
639+# Let's make sure our new descriptors don't conflict with the FK related_name.
640+>>> bob.custom_person_related_name.all()
641+[<CustomMembership: Bob is a member of Rock>]
642+
643+### SELF-REFERENTIAL TESTS ###
644+
645+# Let's first create a person who has no friends.
646+>>> tony = PersonSelfRefM2M.objects.create(name="Tony")
647+>>> tony.friends.all()
648+[]
649+
650+# Now let's create another person for Tony to be friends with.
651+>>> chris = PersonSelfRefM2M.objects.create(name="Chris")
652+>>> f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now())
653+
654+# Tony should now show that Chris is his friend.
655+>>> tony.friends.all()
656+[<PersonSelfRefM2M: Chris>]
657+
658+# But we haven't established that Chris is Tony's Friend.
659+>>> chris.friends.all()
660+[]
661+
662+# So let's do that now.
663+>>> f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now())
664+
665+# Having added Chris as a friend, let's make sure that his friend set reflects
666+# that addition.
667+>>> chris.friends.all()
668+[<PersonSelfRefM2M: Tony>]
669+
670+# Chris gets mad and wants to get rid of all of his friends.
671+>>> chris.friends.clear()
672+
673+# Now he should not have any more friends.
674+>>> chris.friends.all()
675+[]
676+
677+# Since this is a symmetrical relation, Tony's friend link is deleted as well.
678+>>> tony.friends.all()
679+[]
680+
681+
682+
683+### QUERY TESTS ###
684+
685+# We can query for the related model by using its attribute name (members, in
686+# this case).
687+>>> Group.objects.filter(members__name='Bob')
688+[<Group: Roll>]
689+
690+# To query through the intermediary model, we specify its model name.
691+# In this case, membership.
692+>>> Group.objects.filter(membership__invite_reason="She was just awesome.")
693+[<Group: Rock>]
694+
695+# If we want to query in the reverse direction by the related model, use its
696+# model name (group, in this case).
697+>>> Person.objects.filter(group__name="Rock")
698+[<Person: Jim>, <Person: Jane>]
699+
700+# If the m2m field has specified a related_name, using that will work.
701+>>> Person.objects.filter(custom__name="Rock")
702+[<Person: Bob>, <Person: Jim>]
703+
704+# To query through the intermediary model in the reverse direction, we again
705+# specify its model name (membership, in this case).
706+>>> Person.objects.filter(membership__invite_reason="She was just awesome.")
707+[<Person: Jane>]
708+
709+# Let's see all of the groups that Jane joined after 1 Jan 2005:
710+>>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane)
711+[<Group: Rock>]
712+
713+# Queries also work in the reverse direction: Now let's see all of the people
714+# that have joined Rock since 1 Jan 2005:
715+>>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock)
716+[<Person: Jim>, <Person: Jane>]
717+
718+# Conceivably, queries through membership could return correct, but non-unique
719+# querysets.  To demonstrate this, we query for all people who have joined a
720+# group after 2004:
721+>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1))
722+[<Person: Jim>, <Person: Jim>, <Person: Jane>]
723+
724+# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
725+>>> [(m.person.name, m.group.name) for m in
726+... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))]
727+[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
728+
729+# QuerySet's distinct() method can correct this problem.
730+>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct()
731+[<Person: Jim>, <Person: Jane>]
732+"""}
733\ No newline at end of file
734Index: tests/regressiontests/m2m_through_regress/__init__.py
735===================================================================
736Index: tests/regressiontests/m2m_through_regress/models.py
737===================================================================
738--- tests/regressiontests/m2m_through_regress/models.py (revision 0)
739+++ tests/regressiontests/m2m_through_regress/models.py (revision 0)
740@@ -0,0 +1,102 @@
741+from django.db import models
742+from datetime import datetime
743+from django.contrib.auth.models import User
744+
745+# Forward declared intermediate model
746+class Membership(models.Model):
747+    person = models.ForeignKey('Person')
748+    group = models.ForeignKey('Group')
749+    date_joined = models.DateTimeField(default=datetime.now)
750+   
751+    def __unicode__(self):
752+        return "%s is a member of %s" % (self.person.name, self.group.name)
753+
754+class UserMembership(models.Model):
755+    user = models.ForeignKey(User)
756+    group = models.ForeignKey('Group')
757+    date_joined = models.DateTimeField(default=datetime.now)
758+   
759+    def __unicode__(self):
760+        return "%s is a user and member of %s" % (self.user.username, self.group.name)
761+
762+class Person(models.Model):
763+    name = models.CharField(max_length=128)
764+
765+    def __unicode__(self):
766+        return self.name
767+
768+class Group(models.Model):
769+    name = models.CharField(max_length=128)
770+    # Membership object defined as a class
771+    members = models.ManyToManyField(Person, through=Membership)
772+    user_members = models.ManyToManyField(User, through='UserMembership')
773+   
774+    def __unicode__(self):
775+        return self.name
776+       
777+__test__ = {'API_TESTS':"""
778+# Create some dummy data
779+>>> bob = Person.objects.create(name='Bob')
780+>>> jim = Person.objects.create(name='Jim')
781+
782+>>> rock = Group.objects.create(name='Rock')
783+>>> roll = Group.objects.create(name='Roll')
784+
785+>>> frank = User.objects.create_user('frank','frank@example.com','password')
786+>>> jane = User.objects.create_user('jane','jane@example.com','password')
787+
788+# Now test that the forward declared Membership works
789+>>> Membership.objects.create(person=bob, group=rock)
790+<Membership: Bob is a member of Rock>
791+
792+>>> Membership.objects.create(person=bob, group=roll)
793+<Membership: Bob is a member of Roll>
794+
795+>>> Membership.objects.create(person=jim, group=rock)
796+<Membership: Jim is a member of Rock>
797+
798+>>> bob.group_set.all()
799+[<Group: Rock>, <Group: Roll>]
800+
801+>>> roll.members.all()
802+[<Person: Bob>]
803+
804+# Error messages use the model name, not repr of the class name
805+>>> bob.group_set = []
806+Traceback (most recent call last):
807+...
808+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
809+
810+>>> roll.members = []
811+Traceback (most recent call last):
812+...
813+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
814+
815+>>> rock.members.create(name='Anne')
816+Traceback (most recent call last):
817+...
818+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
819+
820+>>> bob.group_set.create(name='Funk')
821+Traceback (most recent call last):
822+...
823+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
824+
825+# Now test that the intermediate with a relationship outside
826+# the current app (i.e., UserMembership) workds
827+>>> UserMembership.objects.create(user=frank, group=rock)
828+<UserMembership: frank is a user and member of Rock>
829+
830+>>> UserMembership.objects.create(user=frank, group=roll)
831+<UserMembership: frank is a user and member of Roll>
832+
833+>>> UserMembership.objects.create(user=jane, group=rock)
834+<UserMembership: jane is a user and member of Rock>
835+
836+>>> frank.group_set.all()
837+[<Group: Rock>, <Group: Roll>]
838+
839+>>> roll.user_members.all()
840+[<User: frank>]
841+
842+"""}
843\ No newline at end of file
844Index: AUTHORS
845===================================================================
846--- AUTHORS     (revision 8090)
847+++ AUTHORS     (working copy)
848@@ -154,6 +154,7 @@
849     Maciej Fijalkowski
850     Matthew Flanagan <http://wadofstuff.blogspot.com>
851     Eric Floehr <eric@intellovations.com>
852+    Eric Florenzano <floguy@gmail.com>
853     Vincent Foley <vfoleybourgon@yahoo.ca>
854     Rudolph Froger <rfroger@estrate.nl>
855     Jorge Gajon <gajon@gajon.org>
856Index: docs/model-api.txt
857===================================================================
858--- docs/model-api.txt  (revision 8090)
859+++ docs/model-api.txt  (working copy)
860@@ -945,6 +945,112 @@
861 
862     =======================  ============================================================
863 
864+Extra fields on many-to-many relationships
865+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
866+
867+When you're only dealing with mixing and matching pizzas and toppings, a
868+standard ``ManyToManyField`` works great. However, sometimes you may want
869+to associated data with the relationship between two models.
870+
871+For example, consider the case of an application tracking the musical groups
872+which musicians belong to. There is a many-to-many relationship between a person
873+and the groups of which they are a member, so you could use a ManyToManyField
874+to represent this relationship. However, there is a lot of detail about the
875+membership that you might want to collect, such as the date at which the person
876+joined the group.
877+
878+For these situations, Django allows you to specify the model that will be used
879+to govern the many-to-many relationship. You can then put extra fields on the
880+intermediate model. The intermediate model is associated with the
881+``ManyToManyField`` by using the ``through`` argument to point the model that
882+will act as an intermediary. For our musician example, the code would look
883+something like this::
884+
885+    class Person(models.Model):
886+        # ...
887+        name = models.CharField(max_length=128)
888+
889+        def __unicode__(self):
890+            return self.name
891+
892+    class Group(models.Model):
893+        # ...
894+        name = models.CharField(max_length=128)
895+        members = models.ManyToManyField(Person, through='Membership')
896+
897+        def __unicode__(self):
898+            return self.name
899+
900+    class Membership(models.Model):
901+        person = models.ForeignKey(Person)
902+        group = models.ForeignKey(Group)
903+        date_joined = models.DateField()
904+        invite_reason = models.CharField(max_length=64)
905+
906+When you set up the intermediary model, you must explicitly specify foreign
907+keys to the models in ManyToMany relation. This explicit declaration makes
908+it clear how two models are related.
909+
910+Now that you have set up your ``ManyToManyField`` to use your intermediary
911+model (Membership, in this case), you're ready to use the convenience methods
912+provided by that ``ManyToManyField``.  Here's an example of how you can query
913+for and use these models::
914+   
915+    >>> ringo = Person.objects.create(name="Ringo Starr")
916+    >>> paul = Person.objects.create(name="Paul McCartney")
917+    >>> beatles = Group.objects.create(name="The Beatles")
918+    >>> m1 = Membership.objects.create(person=ringo, group=beatles,
919+    ...     date_joined=date(1962, 8, 16),
920+    ...     invite_reason= "Needed a new drummer.")
921+    >>> beatles.members.all()
922+    [<Person: Ringo Starr>]
923+    >>> ringo.group_set.all()
924+    [<Group: The Beatles>]
925+    >>> m2 = Membership.objects.create(person=paul, group=beatles,
926+    ...     date_joined=date(1960, 8, 1),
927+    ...     invite_reason= "Wanted to form a band.")
928+    >>> beatles.members.all()
929+    [<Person: Ringo Starr>, <Person: Paul McCartney>]
930+
931+Unlike normal many-to-many fields, you *can't* use ``add``, ``create``,
932+or assignment (i.e., ``beatles.members = [...]``) to create relationships::
933+
934+    # THIS WILL NOT WORK
935+    >>> beatles.members.add(john)
936+    # NEITHER WILL THIS
937+    >>> beatles.members.create(name="George Harrison")
938+    # AND NEITHER WILL THIS
939+    >>> beatles.members = [john, paul, ringo, george]
940+   
941+Why? You can't just create a relationship between a Person and a Group - you
942+need to specify all the detail for the relationship required by the
943+Membership table. The simple ``add``, ``create`` and assignment calls
944+don't provide a way to specify this extra detail. As a result, they are
945+disabled for many-to-many relationships that use an intermediate model.
946+The only way to create a many-to-many relationship with an intermediate table
947+is to create instances of the intermediate model.
948+
949+The ``remove`` method is disabled for similar reasons. However, the
950+``clear()`` method can be used to remove all many-to-many relationships
951+for an instance::
952+
953+    # Beatles have broken up
954+    >>> beatles.members.clear()
955+
956+Once you have established the many-to-many relationships by creating instances
957+of your intermediate model, you can issue queries. You can query using the
958+attributes of the many-to-many-related model::
959+
960+    # Find all the people in the Beatles whose name starts with 'Paul'
961+    >>> beatles.objects.filter(person__name__startswith='Paul')
962+    [<Person: Paul McCartney>]
963+
964+You can also query on the attributes of the intermediate model::
965+
966+    # Find all the members of the Beatles that joined after 1 Jan 1961
967+    >>> beatles.objects.filter(membership__date_joined__gt=date(1961,1,1))
968+    [<Person: Ringo Starr]
969+   
970 One-to-one relationships
971 ~~~~~~~~~~~~~~~~~~~~~~~~
972