Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 8090)
+++ django/db/models/fields/related.py	(working copy)
@@ -23,7 +23,7 @@
 
 pending_lookups = {}
 
-def add_lazy_relation(cls, field, relation):
+def add_lazy_relation(cls, field, relation, operation):
     """
     Adds a lookup on ``cls`` when a related field is defined using a string,
     i.e.::
@@ -45,6 +45,8 @@
     If the other model hasn't yet been loaded -- almost a given if you're using
     lazy relationships -- then the relation won't be set up until the
     class_prepared signal fires at the end of model initialization.
+    
+    operation is the work that must be performed once the relation can be resolved.
     """
     # Check for recursive relations
     if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
@@ -66,11 +68,10 @@
     # is prepared.
     model = get_model(app_label, model_name, False)
     if model:
-        field.rel.to = model
-        field.do_related_class(model, cls)
+        operation(field, model, cls)
     else:
         key = (app_label, model_name)
-        value = (cls, field)
+        value = (cls, field, operation)
         pending_lookups.setdefault(key, []).append(value)
 
 def do_pending_lookups(sender):
@@ -78,9 +79,8 @@
     Handle any pending relations to the sending model. Sent from class_prepared.
     """
     key = (sender._meta.app_label, sender.__name__)
-    for cls, field in pending_lookups.pop(key, []):
-        field.rel.to = sender
-        field.do_related_class(sender, cls)
+    for cls, field, operation in pending_lookups.pop(key, []):
+        operation(field, sender, cls)
 
 dispatcher.connect(do_pending_lookups, signal=signals.class_prepared)
 
@@ -108,7 +108,10 @@
 
         other = self.rel.to
         if isinstance(other, basestring):
-            add_lazy_relation(cls, self, other)
+            def resolve_related_class(field, model, cls):
+                field.rel.to = model
+                field.do_related_class(model, cls)
+            add_lazy_relation(cls, self, other, resolve_related_class)
         else:
             self.do_related_class(other, cls)
 
@@ -339,7 +342,7 @@
             manager.clear()
         manager.add(*value)
 
-def create_many_related_manager(superclass):
+def create_many_related_manager(superclass, through=False):
     """Creates a manager that subclasses 'superclass' (which is a Manager)
     and adds behavior for many-to-many related objects."""
     class ManyRelatedManager(superclass):
@@ -353,6 +356,7 @@
             self.join_table = join_table
             self.source_col_name = source_col_name
             self.target_col_name = target_col_name
+            self.through = through
             self._pk_val = self.instance._get_pk_val()
             if self._pk_val is None:
                 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
@@ -360,21 +364,24 @@
         def get_query_set(self):
             return superclass.get_query_set(self).filter(**(self.core_filters))
 
-        def add(self, *objs):
-            self._add_items(self.source_col_name, self.target_col_name, *objs)
+        # If the ManyToMany relation has an intermediary model, 
+        # the add and remove methods do not exist.
+        if through is None:
+            def add(self, *objs):
+                self._add_items(self.source_col_name, self.target_col_name, *objs)
 
-            # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
-            if self.symmetrical:
-                self._add_items(self.target_col_name, self.source_col_name, *objs)
-        add.alters_data = True
+                # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
+                if self.symmetrical:
+                    self._add_items(self.target_col_name, self.source_col_name, *objs)
+            add.alters_data = True
 
-        def remove(self, *objs):
-            self._remove_items(self.source_col_name, self.target_col_name, *objs)
+            def remove(self, *objs):
+                self._remove_items(self.source_col_name, self.target_col_name, *objs)
 
-            # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
-            if self.symmetrical:
-                self._remove_items(self.target_col_name, self.source_col_name, *objs)
-        remove.alters_data = True
+                # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
+                if self.symmetrical:
+                    self._remove_items(self.target_col_name, self.source_col_name, *objs)
+            remove.alters_data = True
 
         def clear(self):
             self._clear_items(self.source_col_name)
@@ -385,6 +392,10 @@
         clear.alters_data = True
 
         def create(self, **kwargs):
+            # This check needs to be done here, since we can't later remove this
+            # from the method lookup table, as we do with add and remove.
+            if through is not None:
+                raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
             new_obj = self.model(**kwargs)
             new_obj.save()
             self.add(new_obj)
@@ -472,7 +483,7 @@
         # model's default manager.
         rel_model = self.related.model
         superclass = rel_model._default_manager.__class__
-        RelatedManager = create_many_related_manager(superclass)
+        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
 
         qn = connection.ops.quote_name
         manager = RelatedManager(
@@ -491,6 +502,10 @@
         if instance is None:
             raise AttributeError, "Manager must be accessed via instance"
 
+        through = getattr(self.related.field.rel, 'through', None)
+        if through is not None:
+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
+
         manager = self.__get__(instance)
         manager.clear()
         manager.add(*value)
@@ -513,7 +528,7 @@
         # model's default manager.
         rel_model=self.field.rel.to
         superclass = rel_model._default_manager.__class__
-        RelatedManager = create_many_related_manager(superclass)
+        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
 
         qn = connection.ops.quote_name
         manager = RelatedManager(
@@ -532,6 +547,10 @@
         if instance is None:
             raise AttributeError, "Manager must be accessed via instance"
 
+        through = getattr(self.field.rel, 'through', None)
+        if through is not None:
+            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s's Manager instead." % through
+
         manager = self.__get__(instance)
         manager.clear()
         manager.add(*value)
@@ -583,7 +602,7 @@
 
 class ManyToManyRel(object):
     def __init__(self, to, num_in_admin=0, related_name=None,
-        limit_choices_to=None, symmetrical=True):
+        limit_choices_to=None, symmetrical=True, through=None):
         self.to = to
         self.num_in_admin = num_in_admin
         self.related_name = related_name
@@ -593,7 +612,9 @@
         self.edit_inline = False
         self.symmetrical = symmetrical
         self.multiple = True
+        self.through = through
 
+
 class ForeignKey(RelatedField, Field):
     empty_strings_allowed = False
     def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
@@ -718,12 +739,21 @@
 class ManyToManyField(RelatedField, Field):
     def __init__(self, to, **kwargs):
         kwargs['verbose_name'] = kwargs.get('verbose_name', None)
+        self.symmetrical_specified = kwargs.get('symmetrical', None) is not None
         kwargs['rel'] = ManyToManyRel(to,
             num_in_admin=kwargs.pop('num_in_admin', 0),
             related_name=kwargs.pop('related_name', None),
             limit_choices_to=kwargs.pop('limit_choices_to', None),
-            symmetrical=kwargs.pop('symmetrical', True))
+            symmetrical=kwargs.pop('symmetrical', True),
+            through=kwargs.pop('through', None))
+            
         self.db_table = kwargs.pop('db_table', None)
+        if kwargs['rel'].through is not None:
+            self.creates_table = False
+            assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
+        else:
+            self.creates_table = True
+
         Field.__init__(self, **kwargs)
 
         msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
@@ -738,23 +768,44 @@
 
     def _get_m2m_db_table(self, opts):
         "Function that can be curried to provide the m2m table name for this relation"
-        if self.db_table:
+        if self.rel.through is not None:
+            return self.rel.through_model._meta.db_table
+        elif self.db_table:
             return self.db_table
         else:
             return '%s_%s' % (opts.db_table, self.name)
 
     def _get_m2m_column_name(self, related):
         "Function that can be curried to provide the source column name for the m2m table"
+        if self.rel.through is not None:
+            for f in self.rel.through_model._meta.fields:
+                if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
+                    return f.column
         # If this is an m2m relation to self, avoid the inevitable name clash
-        if related.model == related.parent_model:
+        elif related.model == related.parent_model:
             return 'from_' + related.model._meta.object_name.lower() + '_id'
         else:
             return related.model._meta.object_name.lower() + '_id'
 
     def _get_m2m_reverse_name(self, related):
         "Function that can be curried to provide the related column name for the m2m table"
+        if self.rel.through is not None:
+            found = False
+            for f in self.rel.through_model._meta.fields:
+                if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
+                    if related.model == related.parent_model:
+                        # If this is an m2m-intermediate to self, 
+                        # the first foreign key you find will be 
+                        # the source column. Keep searching for
+                        # the second foreign key.
+                        if found:
+                            return f.column
+                        else:
+                            found = True
+                    else:
+                        return f.column
         # If this is an m2m relation to self, avoid the inevitable name clash
-        if related.model == related.parent_model:
+        elif related.model == related.parent_model:
             return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
         else:
             return related.parent_model._meta.object_name.lower() + '_id'
@@ -797,7 +848,17 @@
 
         # Set up the accessor for the m2m table name for the relation
         self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
-
+        
+        # Populate some necessary rel arguments so that cross-app relations
+        # work correctly.
+        if isinstance(self.rel.through, basestring):
+            def resolve_through_model(field, model, cls):
+                field.rel.through_model = model
+            add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
+        elif self.rel.through:
+            self.rel.through_model = self.rel.through
+            self.rel.through = self.rel.through._meta.object_name
+            
         if isinstance(self.rel.to, basestring):
             target = self.rel.to
         else:
Index: django/core/management/validation.py
===================================================================
--- django/core/management/validation.py	(revision 8090)
+++ django/core/management/validation.py	(working copy)
@@ -102,6 +102,7 @@
                         if r.get_accessor_name() == rel_query_name:
                             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))
 
+        seen_intermediary_signatures = [] 
         for i, f in enumerate(opts.local_many_to_many):
             # Check to see if the related m2m field will clash with any
             # existing fields, m2m fields, m2m related objects or related
@@ -112,7 +113,49 @@
                 # so skip the next section
                 if isinstance(f.rel.to, (str, unicode)):
                     continue
-
+            if getattr(f.rel, 'through', None) is not None:
+                if hasattr(f.rel, 'through_model'):
+                    if f.symmetrical_specified:
+                        e.add(opts, "The symmetrical field has no meaning on m2m relations with intermediary models.")
+                    from_model, to_model = cls, f.rel.to
+                    seen_from, seen_to, seen_self = False, False, 0
+                    for inter_field in f.rel.through_model._meta.fields:
+                        rel_to = getattr(inter_field.rel, 'to', None)
+                        if from_model == to_model:
+                            if rel_to == from_model:
+                                seen_self += 1
+                            if seen_self > 2:
+                                e.add(opts, "Intermediary model %s has more than two foreign keys through itself, which is ambiguous and is not permitted." % f.rel.through_model._meta.object_name)
+                        else:
+                            if rel_to == from_model:
+                                if seen_from:
+                                    e.add(opts, "Intermediary model %s has more than one foreign key to one of its sides, %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_from._meta.object_name))
+                                else:
+                                    seen_from = True
+                            elif rel_to == to_model:
+                                if seen_to:
+                                    e.add(opts, "Intermediary model %s has more than one foreign key to one of its sides, %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name))
+                                else:
+                                    seen_to = True
+                    if f.rel.through_model not in models.get_models():
+                        e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
+                    signature = (f.rel.to, cls, f.rel.through_model)
+                    if signature in seen_intermediary_signatures:
+                        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, f.rel.through_model._meta.object_name))
+                    else:
+                        seen_intermediary_signatures.append(signature)
+                    seen_related_fk, seen_this_fk = False, False
+                    for field in f.rel.through_model._meta.fields:
+                        if field.rel:
+                            if not seen_related_fk and field.rel.to == f.rel.to:
+                                seen_related_fk = True
+                            elif field.rel.to == cls:
+                                seen_this_fk = True
+                    if not seen_related_fk or not seen_this_fk:
+                        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))
+                else:
+                    e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
+            
             rel_opts = f.rel.to._meta
             rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
             rel_query_name = f.related_query_name()
Index: django/core/management/sql.py
===================================================================
--- django/core/management/sql.py	(revision 8090)
+++ django/core/management/sql.py	(working copy)
@@ -353,7 +353,7 @@
     qn = connection.ops.quote_name
     inline_references = connection.features.inline_fk_references
     for f in opts.local_many_to_many:
-        if not isinstance(f.rel, generic.GenericRel):
+        if f.creates_table:
             tablespace = f.db_tablespace or opts.db_tablespace
             if tablespace and connection.features.supports_tablespaces: 
                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 8090)
+++ django/contrib/admin/options.py	(working copy)
@@ -161,7 +161,10 @@
                 kwargs['empty_label'] = db_field.blank and _('None') or None
             else:
                 if isinstance(db_field, models.ManyToManyField):
-                    if db_field.name in self.raw_id_fields:
+                    # If it uses an intermediary model, don't show field in admin. 
+                    if db_field.rel.through is not None:
+                        return None
+                    elif db_field.name in self.raw_id_fields:
                         kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
                         kwargs['help_text'] = ''
                     elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
Index: django/contrib/contenttypes/generic.py
===================================================================
--- django/contrib/contenttypes/generic.py	(revision 8090)
+++ django/contrib/contenttypes/generic.py	(working copy)
@@ -104,6 +104,9 @@
                             limit_choices_to=kwargs.pop('limit_choices_to', None),
                             symmetrical=kwargs.pop('symmetrical', True))
 
+        # By its very nature, a GenericRelation doesn't create a table.
+        self.creates_table = False
+
         # Override content-type/object-id field names on the related class
         self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
         self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
Index: tests/modeltests/invalid_models/models.py
===================================================================
--- tests/modeltests/invalid_models/models.py	(revision 8090)
+++ tests/modeltests/invalid_models/models.py	(working copy)
@@ -110,7 +110,55 @@
 class MissingRelations(models.Model):
     rel1 = models.ForeignKey("Rel1")
     rel2 = models.ManyToManyField("Rel2")
+    
+class MissingManualM2MModel(models.Model):
+    name = models.CharField(max_length=5)
+    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
+    
+class Person(models.Model):
+    name = models.CharField(max_length=5)
 
+class Group(models.Model):
+    name = models.CharField(max_length=5)
+    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
+    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
+    tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary")
+
+class GroupTwo(models.Model):
+    name = models.CharField(max_length=5)
+    primary = models.ManyToManyField(Person, through="Membership")
+    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
+
+class Membership(models.Model):
+    person = models.ForeignKey(Person)
+    group = models.ForeignKey(Group)
+    not_default_or_null = models.CharField(max_length=5)
+
+class MembershipMissingFK(models.Model):
+    person = models.ForeignKey(Person)
+
+class PersonSelfRefM2M(models.Model):
+    name = models.CharField(max_length=5)
+    friends = models.ManyToManyField('self', through="Relationship", symmetrical=True)
+    too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK")
+
+class Relationship(models.Model):
+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
+    date_added = models.DateTimeField()
+
+class RelationshipTripleFK(models.Model):
+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2")
+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2")
+    third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far")
+    date_added = models.DateTimeField()
+
+class RelationshipDoubleFK(models.Model):
+    first = models.ForeignKey(Person, related_name="first_related_name")
+    second = models.ForeignKey(Person, related_name="second_related_name")
+    third = models.ForeignKey(Group, related_name="rel_to_set")
+    date_added = models.DateTimeField()
+
 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
 invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
 invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
@@ -195,4 +243,11 @@
 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'.
 invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
 invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
+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.
+invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
+invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
+invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed
+invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to one of its sides, Person, which is ambiguous and is not permitted.
+invalid_models.personselfrefm2m: The symmetrical field has no meaning on m2m relations with intermediary models.
+invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys through itself, which is ambiguous and is not permitted.
 """
Index: tests/modeltests/m2m_through/__init__.py
===================================================================
--- tests/modeltests/m2m_through/__init__.py	(revision 0)
+++ tests/modeltests/m2m_through/__init__.py	(revision 0)
@@ -0,0 +1 @@
+
Index: tests/modeltests/m2m_through/models.py
===================================================================
--- tests/modeltests/m2m_through/models.py	(revision 0)
+++ tests/modeltests/m2m_through/models.py	(revision 0)
@@ -0,0 +1,328 @@
+from django.db import models
+from datetime import datetime
+
+# M2M described on one of the models
+class Person(models.Model):
+    name = models.CharField(max_length=128)
+
+    def __unicode__(self):
+        return self.name
+
+class Group(models.Model):
+    name = models.CharField(max_length=128)
+    members = models.ManyToManyField(Person, through='Membership')
+    custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom")
+    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
+    
+    def __unicode__(self):
+        return self.name
+
+class Membership(models.Model):
+    person = models.ForeignKey(Person)
+    group = models.ForeignKey(Group)
+    date_joined = models.DateTimeField(default=datetime.now)
+    invite_reason = models.CharField(max_length=64, null=True)
+    
+    def __unicode__(self):
+        return "%s is a member of %s" % (self.person.name, self.group.name)
+
+class CustomMembership(models.Model):
+    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
+    group = models.ForeignKey(Group)
+    weird_fk = models.ForeignKey(Membership, null=True)
+    date_joined = models.DateTimeField(default=datetime.now)
+    
+    def __unicode__(self):
+        return "%s is a member of %s" % (self.person.name, self.group.name)
+    
+    class Meta:
+        db_table = "test_table"
+
+class TestNoDefaultsOrNulls(models.Model):
+    person = models.ForeignKey(Person)
+    group = models.ForeignKey(Group)
+    nodefaultnonull = models.CharField(max_length=5)
+
+class PersonSelfRefM2M(models.Model):
+    name = models.CharField(max_length=5)
+    friends = models.ManyToManyField('self', through="Friendship")
+    
+    def __unicode__(self):
+        return self.name
+
+class Friendship(models.Model):
+    first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
+    second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
+    date_friended = models.DateTimeField()
+
+__test__ = {'API_TESTS':"""
+>>> from datetime import datetime
+
+### Creation and Saving Tests ###
+
+>>> bob = Person.objects.create(name='Bob')
+>>> jim = Person.objects.create(name='Jim')
+>>> jane = Person.objects.create(name='Jane')
+>>> rock = Group.objects.create(name='Rock')
+>>> roll = Group.objects.create(name='Roll')
+
+# We start out by making sure that the Group 'rock' has no members.
+>>> rock.members.all()
+[]
+
+# To make Jim a member of Group Rock, simply create a Membership object.
+>>> m1 = Membership.objects.create(person=jim, group=rock)
+
+# We can do the same for Jane and Rock.
+>>> m2 = Membership.objects.create(person=jane, group=rock)
+
+# Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
+>>> rock.members.all()
+[<Person: Jim>, <Person: Jane>]
+
+# Now we can add a bunch more Membership objects to test with.
+>>> m3 = Membership.objects.create(person=bob, group=roll)
+>>> m4 = Membership.objects.create(person=jim, group=roll)
+>>> m5 = Membership.objects.create(person=jane, group=roll)
+
+# We can get Jim's Group membership as with any ForeignKey.
+>>> jim.group_set.all()
+[<Group: Rock>, <Group: Roll>]
+
+# Querying the intermediary model works like normal.  
+# In this case we get Jane's membership to Rock.
+>>> m = Membership.objects.get(person=jane, group=rock)
+>>> m
+<Membership: Jane is a member of Rock>
+
+# Now we set some date_joined dates for further testing.
+>>> m2.invite_reason = "She was just awesome."
+>>> m2.date_joined = datetime(2006, 1, 1)
+>>> m2.save()
+
+>>> m5.date_joined = datetime(2004, 1, 1)
+>>> m5.save()
+
+>>> m3.date_joined = datetime(2004, 1, 1)
+>>> m3.save()
+
+# It's not only get that works. Filter works like normal as well.
+>>> Membership.objects.filter(person=jim)
+[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
+
+
+### Forward Descriptors Tests ###
+
+# Due to complications with adding via an intermediary model, 
+# the add method is not provided.
+>>> rock.members.add(bob)
+Traceback (most recent call last):
+...
+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
+
+# Create is also disabled as it suffers from the same problems as add.
+>>> rock.members.create(name='Anne')
+Traceback (most recent call last):
+...
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+
+# Remove has similar complications, and is not provided either.
+>>> rock.members.remove(jim)
+Traceback (most recent call last):
+...
+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
+
+# Here we back up the list of all members of Rock.
+>>> backup = list(rock.members.all())
+
+# ...and we verify that it has worked.
+>>> backup
+[<Person: Jim>, <Person: Jane>]
+
+# The clear function should still work.
+>>> rock.members.clear()
+
+# Now there will be no members of Rock.
+>>> rock.members.all()
+[]
+
+# Assignment should not work with models specifying a through model for many of
+# the same reasons as adding.
+>>> rock.members = backup
+Traceback (most recent call last):
+...
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
+
+# Let's re-save those instances that we've cleared.
+>>> m1.save()
+>>> m2.save()
+
+# Verifying that those instances were re-saved successfully.
+>>> rock.members.all()
+[<Person: Jim>, <Person: Jane>]
+
+
+### Reverse Descriptors Tests ###
+
+# Due to complications with adding via an intermediary model, 
+# the add method is not provided.
+>>> bob.group_set.add(rock)
+Traceback (most recent call last):
+...
+AttributeError: 'ManyRelatedManager' object has no attribute 'add'
+
+# Create is also disabled as it suffers from the same problems as add.
+>>> bob.group_set.create(name='Funk')
+Traceback (most recent call last):
+...
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+
+# Remove has similar complications, and is not provided either.
+>>> jim.group_set.remove(rock)
+Traceback (most recent call last):
+...
+AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
+
+# Here we back up the list of all of Jim's groups.
+>>> backup = list(jim.group_set.all())
+>>> backup
+[<Group: Rock>, <Group: Roll>]
+
+# The clear function should still work.
+>>> jim.group_set.clear()
+
+# Now Jim will be in no groups.
+>>> jim.group_set.all()
+[]
+
+# Assignment should not work with models specifying a through model for many of
+# the same reasons as adding.
+>>> jim.group_set = backup
+Traceback (most recent call last):
+...
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
+
+# Let's re-save those instances that we've cleared.
+>>> m1.save()
+>>> m4.save()
+
+# Verifying that those instances were re-saved successfully.
+>>> jim.group_set.all()
+[<Group: Rock>, <Group: Roll>]
+
+### Custom Tests ###
+
+# Let's see if we can query through our second relationship.
+>>> rock.custom_members.all()
+[]
+
+# We can query in the opposite direction as well.
+>>> bob.custom.all()
+[]
+
+# Let's create some membership objects in this custom relationship.
+>>> cm1 = CustomMembership.objects.create(person=bob, group=rock)
+>>> cm2 = CustomMembership.objects.create(person=jim, group=rock)
+
+# If we get the number of people in Rock, it should be both Bob and Jim.
+>>> rock.custom_members.all()
+[<Person: Bob>, <Person: Jim>]
+
+# Bob should only be in one custom group.
+>>> bob.custom.all()
+[<Group: Rock>]
+
+# Let's make sure our new descriptors don't conflict with the FK related_name.
+>>> bob.custom_person_related_name.all()
+[<CustomMembership: Bob is a member of Rock>]
+
+### SELF-REFERENTIAL TESTS ###
+
+# Let's first create a person who has no friends.
+>>> tony = PersonSelfRefM2M.objects.create(name="Tony")
+>>> tony.friends.all()
+[]
+
+# Now let's create another person for Tony to be friends with.
+>>> chris = PersonSelfRefM2M.objects.create(name="Chris")
+>>> f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now())
+
+# Tony should now show that Chris is his friend.
+>>> tony.friends.all()
+[<PersonSelfRefM2M: Chris>]
+
+# But we haven't established that Chris is Tony's Friend.
+>>> chris.friends.all()
+[]
+
+# So let's do that now.
+>>> f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now())
+
+# Having added Chris as a friend, let's make sure that his friend set reflects
+# that addition.
+>>> chris.friends.all()
+[<PersonSelfRefM2M: Tony>]
+
+# Chris gets mad and wants to get rid of all of his friends.
+>>> chris.friends.clear()
+
+# Now he should not have any more friends.
+>>> chris.friends.all()
+[]
+
+# Since this is a symmetrical relation, Tony's friend link is deleted as well.
+>>> tony.friends.all()
+[]
+
+
+
+### QUERY TESTS ###
+
+# We can query for the related model by using its attribute name (members, in 
+# this case).
+>>> Group.objects.filter(members__name='Bob')
+[<Group: Roll>]
+
+# To query through the intermediary model, we specify its model name.
+# In this case, membership.
+>>> Group.objects.filter(membership__invite_reason="She was just awesome.")
+[<Group: Rock>]
+
+# If we want to query in the reverse direction by the related model, use its
+# model name (group, in this case).
+>>> Person.objects.filter(group__name="Rock")
+[<Person: Jim>, <Person: Jane>]
+
+# If the m2m field has specified a related_name, using that will work.
+>>> Person.objects.filter(custom__name="Rock")
+[<Person: Bob>, <Person: Jim>]
+
+# To query through the intermediary model in the reverse direction, we again
+# specify its model name (membership, in this case).
+>>> Person.objects.filter(membership__invite_reason="She was just awesome.")
+[<Person: Jane>]
+
+# Let's see all of the groups that Jane joined after 1 Jan 2005:
+>>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane)
+[<Group: Rock>]
+
+# Queries also work in the reverse direction: Now let's see all of the people 
+# that have joined Rock since 1 Jan 2005:
+>>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock)
+[<Person: Jim>, <Person: Jane>]
+
+# Conceivably, queries through membership could return correct, but non-unique
+# querysets.  To demonstrate this, we query for all people who have joined a 
+# group after 2004:
+>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1))
+[<Person: Jim>, <Person: Jim>, <Person: Jane>]
+
+# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
+>>> [(m.person.name, m.group.name) for m in 
+... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))]
+[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
+
+# QuerySet's distinct() method can correct this problem.
+>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct()
+[<Person: Jim>, <Person: Jane>]
+"""}
\ No newline at end of file
Index: tests/regressiontests/m2m_through_regress/__init__.py
===================================================================
--- tests/regressiontests/m2m_through_regress/__init__.py	(revision 0)
+++ tests/regressiontests/m2m_through_regress/__init__.py	(revision 0)
@@ -0,0 +1 @@
+
Index: tests/regressiontests/m2m_through_regress/models.py
===================================================================
--- tests/regressiontests/m2m_through_regress/models.py	(revision 0)
+++ tests/regressiontests/m2m_through_regress/models.py	(revision 0)
@@ -0,0 +1,102 @@
+from django.db import models
+from datetime import datetime
+from django.contrib.auth.models import User
+
+# Forward declared intermediate model
+class Membership(models.Model):
+    person = models.ForeignKey('Person')
+    group = models.ForeignKey('Group')
+    date_joined = models.DateTimeField(default=datetime.now)
+    
+    def __unicode__(self):
+        return "%s is a member of %s" % (self.person.name, self.group.name)
+
+class UserMembership(models.Model):
+    user = models.ForeignKey(User)
+    group = models.ForeignKey('Group')
+    date_joined = models.DateTimeField(default=datetime.now)
+    
+    def __unicode__(self):
+        return "%s is a user and member of %s" % (self.user.username, self.group.name)
+
+class Person(models.Model):
+    name = models.CharField(max_length=128)
+
+    def __unicode__(self):
+        return self.name
+
+class Group(models.Model):
+    name = models.CharField(max_length=128)
+    # Membership object defined as a class
+    members = models.ManyToManyField(Person, through=Membership)
+    user_members = models.ManyToManyField(User, through='UserMembership')
+    
+    def __unicode__(self):
+        return self.name
+        
+__test__ = {'API_TESTS':"""
+# Create some dummy data
+>>> bob = Person.objects.create(name='Bob')
+>>> jim = Person.objects.create(name='Jim')
+
+>>> rock = Group.objects.create(name='Rock')
+>>> roll = Group.objects.create(name='Roll')
+
+>>> frank = User.objects.create_user('frank','frank@example.com','password')
+>>> jane = User.objects.create_user('jane','jane@example.com','password')
+
+# Now test that the forward declared Membership works 
+>>> Membership.objects.create(person=bob, group=rock)
+<Membership: Bob is a member of Rock>
+
+>>> Membership.objects.create(person=bob, group=roll)
+<Membership: Bob is a member of Roll>
+
+>>> Membership.objects.create(person=jim, group=rock)
+<Membership: Jim is a member of Rock>
+
+>>> bob.group_set.all()
+[<Group: Rock>, <Group: Roll>]
+
+>>> roll.members.all()
+[<Person: Bob>]
+
+# Error messages use the model name, not repr of the class name
+>>> bob.group_set = []
+Traceback (most recent call last):
+...
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
+
+>>> roll.members = []
+Traceback (most recent call last):
+...
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
+
+>>> rock.members.create(name='Anne')
+Traceback (most recent call last):
+...
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
+
+>>> bob.group_set.create(name='Funk')
+Traceback (most recent call last):
+...
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model.  Use Membership's Manager instead.
+
+# Now test that the intermediate with a relationship outside 
+# the current app (i.e., UserMembership) workds
+>>> UserMembership.objects.create(user=frank, group=rock)
+<UserMembership: frank is a user and member of Rock>
+
+>>> UserMembership.objects.create(user=frank, group=roll)
+<UserMembership: frank is a user and member of Roll>
+
+>>> UserMembership.objects.create(user=jane, group=rock)
+<UserMembership: jane is a user and member of Rock>
+
+>>> frank.group_set.all()
+[<Group: Rock>, <Group: Roll>]
+
+>>> roll.user_members.all()
+[<User: frank>]
+
+"""}
\ No newline at end of file
Index: AUTHORS
===================================================================
--- AUTHORS	(revision 8090)
+++ AUTHORS	(working copy)
@@ -154,6 +154,7 @@
     Maciej Fijalkowski
     Matthew Flanagan <http://wadofstuff.blogspot.com>
     Eric Floehr <eric@intellovations.com>
+    Eric Florenzano <floguy@gmail.com>
     Vincent Foley <vfoleybourgon@yahoo.ca>
     Rudolph Froger <rfroger@estrate.nl>
     Jorge Gajon <gajon@gajon.org>
Index: docs/model-api.txt
===================================================================
--- docs/model-api.txt	(revision 8090)
+++ docs/model-api.txt	(working copy)
@@ -945,6 +945,112 @@
 
     =======================  ============================================================
 
+Extra fields on many-to-many relationships
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When you're only dealing with mixing and matching pizzas and toppings, a
+standard ``ManyToManyField`` works great. However, sometimes you may want
+to associate data with the relationship between two models. 
+
+For example, consider the case of an application tracking the musical groups
+which musicians belong to. There is a many-to-many relationship between a person
+and the groups of which they are a member, so you could use a ManyToManyField
+to represent this relationship. However, there is a lot of detail about the
+membership that you might want to collect, such as the date at which the person
+joined the group.
+
+For these situations, Django allows you to specify the model that will be used
+to govern the many-to-many relationship. You can then put extra fields on the
+intermediate model. The intermediate model is associated with the 
+``ManyToManyField`` by using the ``through`` argument to point to the model
+that will act as an intermediary. For our musician example, the code would look
+something like this::
+
+    class Person(models.Model):
+        # ...
+        name = models.CharField(max_length=128)
+
+        def __unicode__(self):
+            return self.name
+
+    class Group(models.Model):
+        # ...
+        name = models.CharField(max_length=128)
+        members = models.ManyToManyField(Person, through='Membership')
+
+        def __unicode__(self):
+            return self.name
+
+    class Membership(models.Model):
+        person = models.ForeignKey(Person)
+        group = models.ForeignKey(Group)
+        date_joined = models.DateField()
+        invite_reason = models.CharField(max_length=64)
+
+When you set up the intermediary model, you must explicitly specify foreign 
+keys to the models in ManyToMany relation. This explicit declaration makes 
+it clear how two models are related.
+
+Now that you have set up your ``ManyToManyField`` to use your intermediary 
+model (Membership, in this case), you're ready to use the convenience methods
+provided by that ``ManyToManyField``.  Here's an example of how you can query
+for and use these models::
+    
+    >>> ringo = Person.objects.create(name="Ringo Starr")
+    >>> paul = Person.objects.create(name="Paul McCartney")
+    >>> beatles = Group.objects.create(name="The Beatles")
+    >>> m1 = Membership.objects.create(person=ringo, group=beatles,
+    ...     date_joined=date(1962, 8, 16), 
+    ...     invite_reason= "Needed a new drummer.")
+    >>> beatles.members.all()
+    [<Person: Ringo Starr>]
+    >>> ringo.group_set.all()
+    [<Group: The Beatles>]
+    >>> m2 = Membership.objects.create(person=paul, group=beatles,
+    ...     date_joined=date(1960, 8, 1), 
+    ...     invite_reason= "Wanted to form a band.")
+    >>> beatles.members.all()
+    [<Person: Ringo Starr>, <Person: Paul McCartney>]
+
+Unlike normal many-to-many fields, you *can't* use ``add``, ``create``,
+or assignment (i.e., ``beatles.members = [...]``) to create relationships::
+
+    # THIS WILL NOT WORK
+    >>> beatles.members.add(john)
+    # NEITHER WILL THIS
+    >>> beatles.members.create(name="George Harrison")
+    # AND NEITHER WILL THIS
+    >>> beatles.members = [john, paul, ringo, george]
+    
+Why? You can't just create a relationship between a Person and a Group - you
+need to specify all the detail for the relationship required by the
+Membership table. The simple ``add``, ``create`` and assignment calls
+don't provide a way to specify this extra detail. As a result, they are
+disabled for many-to-many relationships that use an intermediate model.
+The only way to create a many-to-many relationship with an intermediate table
+is to create instances of the intermediate model.
+
+The ``remove`` method is disabled for similar reasons. However, the
+``clear()`` method can be used to remove all many-to-many relationships
+for an instance::
+
+    # Beatles have broken up
+    >>> beatles.members.clear()
+
+Once you have established the many-to-many relationships by creating instances
+of your intermediate model, you can issue queries. You can query using the
+attributes of the many-to-many-related model::
+
+    # Find all the people in the Beatles whose name starts with 'Paul'
+    >>> beatles.objects.filter(person__name__startswith='Paul')
+    [<Person: Paul McCartney>]
+
+You can also query on the attributes of the intermediate model::
+
+    # Find all the members of the Beatles that joined after 1 Jan 1961
+    >>> beatles.objects.filter(membership__date_joined__gt=date(1961,1,1))
+    [<Person: Ringo Starr]
+    
 One-to-one relationships
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
Index: docs/admin.txt
===================================================================
--- docs/admin.txt	(revision 8090)
+++ docs/admin.txt	(working copy)
@@ -613,6 +613,64 @@
             FriendshipInline,
         ]
 
+Working with Many-to-Many Intermediary Models
+----------------------------------------------
+
+By default, admin widgets for many-to-many relations will be displayed inline
+on whichever model contains the actual reference to the `ManyToManyField`.  
+However, as soon as you specify an intermediary model using the ``through`` 
+argument to a ``ManyToManyField``, the admin will no longer display that 
+widget.  The reason for this is because each instance of that intermediary 
+model has more information than could be displayed in any one widget.
+
+But we still want to be able to edit that information inline.  Fortunately,
+this is easy to do with the admin application.  Suppose we have the following
+models::
+
+    class Person(models.Model):
+        name = models.CharField(max_length=128)
+    
+    class Group(models.Model):
+        name = models.CharField(max_length=128)
+        members = models.ManyToManyField(Person, through='Membership')
+
+    class Membership(models.Model):
+        person = models.ForeignKey(Person)
+        group = models.ForeignKey(Group)
+        date_joined = models.DateField()
+        invite_reason = models.CharField(max_length=64)
+
+In this case, it is easy to create an admin to view the ``Person`` and 
+``Group`` models, like so::
+
+    from django.contrib import admin
+    
+    class MembershipAdmin(admin.ModelAdmin):
+        # Nothing here yet.
+        pass
+    
+    admin.site.register(Person, MembershipAdmin)
+    admin.site.register(Group, MembershipAdmin)
+
+Now we need create a custom ``ModelInline`` subclass, like so::
+
+    class MembershipInline(admin.InlineModelAdmin):
+        model = Membership
+        extra = 1
+
+Of course, this subclass could look much different, based on the amount of
+customization that we want to do.  For demonstration purposes, however, this
+simple sublclass will do just fine.  Now, the only thing left to do is ensure
+that the ``MembershipAdmin`` class knows which inline to display.  We can
+rewrite it to look like this::
+
+    class MembershipAdmin(admin.ModelAdmin):
+        inlines = (MembershipInline,)
+
+Now your admin site is set up to edit ``Membership`` objects inline from either
+the ``Person`` or the ``Group`` detail pages.
+    
+
 ``AdminSite`` objects
 =====================
 
