Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 6888)
+++ django/db/models/fields/related.py	(working copy)
@@ -1,10 +1,10 @@
 from django.db import connection, transaction
-from django.db.models import signals, get_model
+from django.db.models import signals, get_model, get_models
 from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, get_ul_class
 from django.db.models.related import RelatedObject
 from django.utils.text import capfirst
 from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _
-from django.utils.functional import curry
+from django.utils.functional import curry, memoize
 from django.utils.encoding import smart_unicode
 from django.core import validators
 from django import oldforms
@@ -23,6 +23,10 @@
 
 pending_lookups = {}
 
+memoized_fk_field_reversals = {}
+
+model_db_table_cache = {}
+
 def add_lookup(rel_cls, field):
     name = field.rel.to
     module = rel_cls.__module__
@@ -54,6 +58,30 @@
     except klass.DoesNotExist:
         raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name
 
+def get_reverse_rel_field(from_model, to_model, related_name):
+    key = (from_model._meta.app_label, from_model._meta.object_name,
+            to_model._meta.app_label, to_model._meta.object_name,
+            related_name)
+    try:
+        found_field = memoized_fk_field_reversals[key]
+    except KeyError:
+        found_field = None
+        for field in from_model._meta.fields:
+            if field.__class__ in (ForeignKey, OneToOneField, ManyToManyField):
+                if field.rel.related_name == related_name:
+                    if field.rel.to == to_model:
+                        found_field = field
+                        break
+        memoized_fk_field_reversals[key] = found_field
+    return found_field
+
+def get_model_for_db_table(db_table):
+    for model in get_models():
+        if model._meta.db_table == db_table:
+            return model
+    return None
+get_model_for_db_table = memoize(get_model_for_db_table, model_db_table_cache, 1)
+
 #HACK
 class RelatedField(object):
     def contribute_to_class(self, cls, name):
@@ -276,6 +304,7 @@
             self.join_table = join_table
             self.source_col_name = source_col_name
             self.target_col_name = target_col_name
+            self.intermediary_model = get_model_for_db_table(self.join_table.replace('"',''))
             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." % model)
@@ -340,9 +369,15 @@
 
                 # Add the ones that aren't there already
                 for obj_id in (new_ids - existing_ids):
-                    cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \
+                    if self.intermediary_model == None:
+                        cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \
                         (self.join_table, source_col_name, target_col_name),
                         [self._pk_val, obj_id])
+                    else:
+                        new_obj = self.intermediary_model()
+                        setattr(new_obj, source_col_name.replace('"', ''), self._pk_val)
+                        setattr(new_obj, target_col_name.replace('"', ''), obj_id)
+                        new_obj.save()
                 transaction.commit_unless_managed()
 
         def _remove_items(self, source_col_name, target_col_name, *objs):
@@ -648,7 +683,8 @@
             filter_interface=kwargs.pop('filter_interface', None),
             limit_choices_to=kwargs.pop('limit_choices_to', None),
             raw_id_admin=kwargs.pop('raw_id_admin', False),
-            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"].raw_id_admin:
             kwargs.setdefault("validator_list", []).append(self.isValidIDList)
@@ -672,7 +708,9 @@
 
     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 != None:
+            return get_model(opts.app_label, self.rel.through)._meta.db_table
+        elif self.db_table:
             return self.db_table
         else:
             return '%s_%s' % (opts.db_table, self.name)
@@ -680,7 +718,12 @@
     def _get_m2m_column_name(self, related):
         "Function that can be curried to provide the source column name for the m2m table"
         # If this is an m2m relation to self, avoid the inevitable name clash
-        if related.model == related.parent_model:
+        if self.rel.through != None:
+            through = get_model(related.opts.app_label, self.rel.through)
+            field = get_reverse_rel_field(through, related.model, self.rel.related_name)
+            attname, column = field.get_attname_column()
+            return column
+        elif related.model == related.parent_model:
             return 'from_' + related.model._meta.object_name.lower() + '_id'
         else:
             return related.model._meta.object_name.lower() + '_id'
@@ -688,7 +731,12 @@
     def _get_m2m_reverse_name(self, related):
         "Function that can be curried to provide the related column name for the m2m table"
         # If this is an m2m relation to self, avoid the inevitable name clash
-        if related.model == related.parent_model:
+        if self.rel.through != None:
+            through = get_model(related.opts.app_label, self.rel.through)
+            field = get_reverse_rel_field(through, related.parent_model, self.rel.related_name)
+            attname, column = field.get_attname_column()
+            return column
+        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'
@@ -809,7 +857,8 @@
 
 class ManyToManyRel(object):
     def __init__(self, to, num_in_admin=0, related_name=None,
-        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True):
+        filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True,
+        through = None):
         self.to = to
         self.num_in_admin = num_in_admin
         self.related_name = related_name
@@ -821,5 +870,6 @@
         self.raw_id_admin = raw_id_admin
         self.symmetrical = symmetrical
         self.multiple = True
+        self.through = through
 
         assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"
Index: django/core/management/sql.py
===================================================================
--- django/core/management/sql.py	(revision 6888)
+++ django/core/management/sql.py	(working copy)
@@ -349,7 +349,7 @@
     qn = connection.ops.quote_name
     inline_references = connection.features.inline_fk_references
     for f in opts.many_to_many:
-        if not isinstance(f.rel, generic.GenericRel):
+        if not isinstance(f.rel, generic.GenericRel) and getattr(f.rel, 'through', None) == None:
             tablespace = f.db_tablespace or opts.db_tablespace
             if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
                 tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
Index: tests/modeltests/m2m_manual/__init__.py
===================================================================
Index: tests/modeltests/m2m_manual/models.py
===================================================================
--- tests/modeltests/m2m_manual/models.py	(revision 0)
+++ tests/modeltests/m2m_manual/models.py	(revision 0)
@@ -0,0 +1,61 @@
+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')
+    
+    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)
+    
+    def __unicode__(self):
+        return "%s is a member of %s" % (self.person.name, self.group.name)
+
+__test__ = {'API_TESTS':"""
+>>> bob = Person(name = 'Bob')
+>>> bob.save()
+>>> jim = Person(name = 'Jim')
+>>> jim.save()
+>>> jane = Person(name = 'Jane')
+>>> jane.save()
+>>> rock = Group(name = 'Rock')
+>>> rock.save()
+>>> roll = Group(name = 'Roll')
+>>> roll.save()
+
+>>> rock.members.add(jim, jane)
+>>> rock.members.all()
+[<Person: Jim>, <Person: Jane>]
+
+>>> roll.members.add(bob, jim)
+>>> roll.members.all()
+[<Person: Bob>, <Person: Jim>]
+
+>>> jane.group_set.all()
+[<Group: Rock>]
+
+>>> jane.group_set.add(roll)
+>>> jane.group_set.all()
+[<Group: Rock>, <Group: Roll>]
+
+>>> jim.group_set.all()
+[<Group: Rock>, <Group: Roll>]
+
+>>> Membership.objects.filter(person = jane, group = rock)
+[<Membership: Jane is a member of Rock>]
+
+>>> Membership.objects.filter(person = jim)
+[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
+"""}
\ No newline at end of file
