Index: django/db/models/options.py
===================================================================
--- django/db/models/options.py	(revision 8068)
+++ django/db/models/options.py	(working copy)
@@ -10,7 +10,7 @@
 from django.db.models.fields.related import ManyToManyRel
 from django.db.models.fields import AutoField, FieldDoesNotExist
 from django.db.models.fields.proxy import OrderWrt
-from django.db.models.loading import get_models, app_cache_ready
+from django.db.models.loading import get_models, get_model, app_cache_ready
 from django.utils.translation import activate, deactivate_all, get_language, string_concat
 from django.utils.encoding import force_unicode, smart_str
 from django.utils.datastructures import SortedDict
@@ -401,6 +401,16 @@
             follow = self.get_follow()
         return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
 
+    def get_related_object(self, from_model, self_ref=False):
+        "Gets the RelatedObject which links from from_model to this model."
+        for related_object in self.get_all_related_objects():
+            if related_object.model == from_model:
+                if self_ref:
+                    self_ref = False
+                else:
+                    return related_object
+        return None
+
     def get_data_holders(self, follow=None):
         if follow == None:
             follow = self.get_follow()
Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 8068)
+++ django/db/models/fields/related.py	(working copy)
@@ -339,7 +339,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 +353,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 +361,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 +389,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 +480,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 +499,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 +525,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 +544,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 +599,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 +609,20 @@
         self.edit_inline = False
         self.symmetrical = symmetrical
         self.multiple = True
+        self.through = through
+        self.through_app_label = None
+        self._model_cache = None
 
+    def _get_through_model(self):
+        if self._model_cache:
+            return self._model_cache
+        if self.through and self.through_app_label:
+            self._model_cache = get_model(self.through_app_label, self.through)
+            return self._model_cache
+        return None
+    through_model = property(_get_through_model)
+
+
 class ForeignKey(RelatedField, Field):
     empty_strings_allowed = False
     def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
@@ -722,8 +751,16 @@
             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 +775,35 @@
 
     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:
+            field = related.model._meta.get_related_object(self.rel.through_model).field
+            return field.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:
+            meta = related.parent_model._meta
+            if self.parent == self.rel.to:
+                related = meta.get_related_object(self.rel.through_model, self_ref=True)
+            else:
+                related = meta.get_related_object(self.rel.through_model)
+            return related.field.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,6 +846,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)
+        
+        # Get a reference to the parent model
+        self.parent = cls
+        
+        # Populate some necessary rel arguments so that cross-app relations
+        # work correctly.
+        if isinstance(self.rel.through, basestring):
+            try:
+                self.rel.through_app_label, self.rel.through = self.rel.through.split('.')
+            except ValueError:
+                self.rel.through_app_label = cls._meta.app_label
 
         if isinstance(self.rel.to, basestring):
             target = self.rel.to
Index: django/core/management/validation.py
===================================================================
--- django/core/management/validation.py	(revision 8068)
+++ 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,29 @@
                 # so skip the next section
                 if isinstance(f.rel.to, (str, unicode)):
                     continue
-
+            if getattr(f.rel, 'through', None) is not None:
+                intermediary_model = None
+                for model in models.get_models():
+                    if model._meta.module_name == f.rel.through.lower():
+                        intermediary_model = model
+                if intermediary_model is None:
+                    e.add(opts, "%s has a manually-defined m2m relation through model %s, which does not exist." % (f.name, f.rel.through))
+                else:
+                    signature = (f.rel.to, cls, intermediary_model)
+                    if signature in seen_intermediary_signatures:
+                        e.add(opts, "%s has two manually-defined m2m relations through model %s, which is not possible.  Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, intermediary_model._meta.object_name))
+                    else:
+                        seen_intermediary_signatures.append(signature)
+                    seen_related_fk, seen_this_fk = False, False
+                    for field in intermediary_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))
+            
             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 8068)
+++ 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/contenttypes/generic.py
===================================================================
--- django/contrib/contenttypes/generic.py	(revision 8068)
+++ 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 8068)
+++ tests/modeltests/invalid_models/models.py	(working copy)
@@ -110,7 +110,41 @@
 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")
+
+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")
+
+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()
+
 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 +229,8 @@
 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: Group has two manually-defined m2m relations through model Membership, which is not possible.  Please consider using an extra field on your intermediary model instead.
+invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relation through model MissingM2MModel, which does not exist.
+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
 """
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
===================================================================
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,80 @@
+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)
+    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>]
+
+# 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 8068)
+++ 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 8068)
+++ docs/model-api.txt	(working copy)
@@ -942,6 +942,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 associated 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 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
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
