Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 11745)
+++ django/db/models/fields/related.py	(working copy)
@@ -411,7 +411,8 @@
     through = rel.through
     class ManyRelatedManager(superclass):
         def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
-                join_table=None, source_field_name=None, target_field_name=None):
+                join_table=None, source_field_name=None, target_field_name=None,
+                field_name=None, reverse=False):
             super(ManyRelatedManager, self).__init__()
             self.core_filters = core_filters
             self.model = model
@@ -421,6 +422,8 @@
             self.target_field_name = target_field_name
             self.through = through
             self._pk_val = self.instance.pk
+            self.field_name = field_name
+            self.reverse = reverse
             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__)
 
@@ -497,15 +500,29 @@
                     source_field_name: self._pk_val,
                     '%s__in' % target_field_name: new_ids,
                 })
-                vals = set(vals)
-
+                new_ids=new_ids-set(vals)
                 # Add the ones that aren't there already
-                for obj_id in (new_ids - vals):
+                for obj_id in new_ids:
                     self.through._default_manager.create(**{
                         '%s_id' % source_field_name: self._pk_val,
                         '%s_id' % target_field_name: obj_id,
                     })
+                added_objs = [obj for obj in objs if \
+                      (isinstance(obj, self.model) and obj._get_pk_val() in new_ids) \
+                      or obj in new_ids]
 
+                if self.reverse:
+                    sender = self.model
+                else:
+                    sender = self.instance.__class__
+                    signals.m2m_changed.send(sender=sender, action='add',
+                        instance=self.instance, model=self.model, 
+                        reverse=self.reverse, field_name=self.field_name, 
+                        objects=added_objs)
+
+
+
+
         def _remove_items(self, source_field_name, target_field_name, *objs):
             # source_col_name: the PK colname in join_table for the source object
             # target_col_name: the PK colname in join_table for the target object
@@ -525,9 +542,24 @@
                     source_field_name: self._pk_val,
                     '%s__in' % target_field_name: old_ids
                 }).delete()
+                if self.reverse: 
+                    sender = self.model 
+                else: 
+                    sender = self.instance.__class__ 
+                signals.m2m_changed.send(sender=sender, action="remove", 
+                    instance=self.instance, model=self.model, 
+                    reverse=self.reverse, field_name=self.field_name,  
+                    objects=list(objs))
 
         def _clear_items(self, source_field_name):
             # source_col_name: the PK colname in join_table for the source object
+            if self.reverse:
+                sender = self.model
+            else:
+                sender = self.instance.__class__
+            signals.m2m_changed.send(sender=sender, action="clear",
+                instance=self.instance, model=self.model, reverse=self.reverse, 
+                field_name=self.field_name, objects=None)
             self.through._default_manager.filter(**{
                 source_field_name: self._pk_val
             }).delete()
@@ -560,7 +592,9 @@
             instance=instance,
             symmetrical=False,
             source_field_name=self.related.field.m2m_reverse_field_name(),
-            target_field_name=self.related.field.m2m_field_name()
+            target_field_name=self.related.field.m2m_field_name(),
+            field_name=self.related.field.name, 
+            reverse=True 
         )
 
         return manager
@@ -610,7 +644,9 @@
             instance=instance,
             symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
             source_field_name=self.field.m2m_field_name(),
-            target_field_name=self.field.m2m_reverse_field_name()
+            target_field_name=self.field.m2m_reverse_field_name(),
+            field_name=self.field.name, 
+            reverse=False
         )
 
         return manager
Index: django/db/models/signals.py
===================================================================
--- django/db/models/signals.py	(revision 11745)
+++ django/db/models/signals.py	(working copy)
@@ -12,3 +12,5 @@
 post_delete = Signal(providing_args=["instance"])
 
 post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
+
+m2m_changed = Signal(providing_args=["instance", "action", "model", "field_name", "reverse", "objects"])
Index: tests/modeltests/m2m_signals/__init__.py
===================================================================
--- tests/modeltests/m2m_signals/__init__.py	(revision 0)
+++ tests/modeltests/m2m_signals/__init__.py	(revision 0)
@@ -0,0 +1 @@
+
Index: tests/modeltests/m2m_signals/models.py
===================================================================
--- tests/modeltests/m2m_signals/models.py	(revision 0)
+++ tests/modeltests/m2m_signals/models.py	(revision 0)
@@ -0,0 +1,166 @@
+"""
+Testing signals emitted on changing m2m relations.
+"""
+
+from django.db import models
+
+class Part(models.Model):
+    name = models.CharField(max_length=20)
+	
+    def __unicode__(self):
+        return self.name
+
+class Car(models.Model):
+    name = models.CharField(max_length=20)
+    default_parts = models.ManyToManyField(Part)
+    optional_parts = models.ManyToManyField(Part, related_name='cars_optional')
+
+    def __unicode__(self):
+        return self.name
+
+def m2m_changed_test(signal, sender, **kwargs):
+    print 'm2m_changed signal'
+    print 'instance:', kwargs['instance']
+    print 'action:', kwargs['action']
+    print 'reverse:', kwargs['reverse']
+    print 'field_name:', kwargs['field_name']
+    print 'model:', kwargs['model']
+    print 'objects:',kwargs['objects']
+    
+
+__test__ = {'API_TESTS':"""
+>>> models.signals.m2m_changed.connect(m2m_changed_test, Car)
+
+# Test the add, remove and clear methods on both sides of the 
+# many-to-many relation
+
+>>> c1 = Car.objects.create(name='VW')
+>>> c2 = Car.objects.create(name='BMW')
+>>> c3 = Car.objects.create(name='Toyota')
+>>> p1 = Part.objects.create(name='Wheelset')
+>>> p2 = Part.objects.create(name='Doors')
+>>> p3 = Part.objects.create(name='Engine')
+>>> p4 = Part.objects.create(name='Airbag')
+>>> p5 = Part.objects.create(name='Sunroof')
+
+# adding some default parts to our car
+>>> c1.default_parts.add(p1, p2, p3)
+m2m_changed signal
+instance: VW
+action: add
+reverse: False
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>]
+
+# give the BMW and Toyata some doors as well
+>>> p2.car_set.add(c2, c3)
+m2m_changed signal
+instance: Doors
+action: add
+reverse: True
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: [<Car: BMW>, <Car: Toyota>]
+
+# remove the engine from the VW and the airbag (which is not set but is returned)
+>>> c1.default_parts.remove(p3, p4)
+m2m_changed signal
+instance: VW
+action: remove
+reverse: False
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Engine>, <Part: Airbag>]
+
+# give the VW some optional parts (second relation to same model)
+>>> c1.optional_parts.add(p4,p5)
+m2m_changed signal
+instance: VW
+action: add
+reverse: False
+field_name: optional_parts
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Airbag>, <Part: Sunroof>]
+
+# add airbag to all the cars (even though the VW already has one)
+>>> p4.cars_optional.add(c1, c2, c3)
+m2m_changed signal
+instance: Airbag
+action: add
+reverse: True
+field_name: optional_parts
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: [<Car: BMW>, <Car: Toyota>]
+
+# remove airbag from the VW (reverse relation with custom related_name)
+>>> p4.cars_optional.remove(c1)
+m2m_changed signal
+instance: Airbag
+action: remove
+reverse: True
+field_name: optional_parts
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: [<Car: VW>]
+
+# clear all parts of the VW 
+>>> c1.default_parts.clear()
+m2m_changed signal
+instance: VW
+action: clear
+reverse: False
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: None
+
+# take all the doors off of cars
+>>> p2.car_set.clear()
+m2m_changed signal
+instance: Doors
+action: clear
+reverse: True
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: None
+
+# take all the airbags off of cars (clear reverse relation with custom related_name)
+>>> p4.cars_optional.clear()
+m2m_changed signal
+instance: Airbag
+action: clear
+reverse: True
+field_name: optional_parts
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: None
+
+# alternative ways of setting relation:
+
+>>> c1.default_parts.create(name='Windows')
+m2m_changed signal
+instance: VW
+action: add
+reverse: False
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Windows>]
+<Part: Windows>
+
+# direct assignment clears the set first, then adds
+>>> c1.default_parts = [p1,p2,p3]
+m2m_changed signal
+instance: VW
+action: clear
+reverse: False
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: None
+m2m_changed signal
+instance: VW
+action: add
+reverse: False
+field_name: default_parts
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>]
+
+>>> models.signals.m2m_changed.disconnect(m2m_changed_test)
+"""}
Index: docs/topics/signals.txt
===================================================================
--- docs/topics/signals.txt	(revision 11745)
+++ docs/topics/signals.txt	(working copy)
@@ -28,7 +28,10 @@
 
       Sent before or after a model's :meth:`~django.db.models.Model.delete`
       method is called.
+      
+    * :data:`django.db.models.signals.m2m_changed`
 
+      Sent when a :class:`ManyToManyField` on a model is changed.
 
     * :data:`django.core.signals.request_started` &
       :data:`django.core.signals.request_finished`
Index: docs/ref/signals.txt
===================================================================
--- docs/ref/signals.txt	(revision 11745)
+++ docs/ref/signals.txt	(working copy)
@@ -170,6 +170,139 @@
         Note that the object will no longer be in the database, so be very
         careful what you do with this instance.
 
+m2m_changed
+-----------
+
+.. data:: django.db.models.signals.m2m_changed
+   :module: 
+
+Sent when a :class:`ManyToManyField` is changed on a model instance. 
+Strictly speaking, this is not a model signal since it is sent by the
+:class:`ManyToManyField`, but since it complements the 
+:data:`pre_save`/:data:`post_save` and :data:`pre_delete`/:data:`post_delete`
+when it comes to tracking changes to models, it is included here.
+
+Arguments sent with this signal:
+
+    ``sender``
+        The model class containing the :class:`ManyToManyField`.
+
+    ``instance``
+        The instance whose many-to-many relation is updated. This can be an
+        instance of the ``sender``, or of the class the :class:`ManyToManyField`
+        is related to.
+
+    ``action``
+        A string indicating the type of update that is done on the relation. 
+        This can be one of the following:
+        
+        ``"add"``
+            Sent *after* one or more objects are added to the relation,
+        ``"remove"``        
+            Sent *after* one or more objects are removed from the relation,
+        ``"clear"``
+            Sent *before* the relation is cleared.
+        
+    ``model``
+        The class of the objects that are added to, removed from or cleared 
+        from the relation.
+        
+    ``field_name``
+        The name of the :class:`ManyToManyField` in the ``sender`` class.
+        This can be used to identify which relation has changed 
+        when multiple many-to-many relations to the same model 
+        exist in ``sender``.
+        
+    ``reverse``
+    	Indicates which side of the relation is updated.
+    	It is ``False`` for updates on an instance of the ``sender`` and
+        ``True`` for updates on an instance of the related class.
+        
+    ``objects``
+        With the ``"add"`` and ``"remove"`` action, this is a list of
+        objects that have been added to or removed from the relation.
+        The class of these objects is given in the ``model`` argument.
+        
+        For the ``"clear"`` action, this is ``None`` .
+        
+        Note that if you pass primary keys to the ``add`` or ``remove`` method
+        of a relation, ``objects`` will contain primary keys, not instances.
+        Also note that if you pass objects to the ``add`` method that are 
+        already in the relation, they will not be in the ``objects`` list.
+        (This doesn't apply to ``remove``.)
+        
+For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled
+like this:
+
+.. code-block:: python
+
+    class Topping(models.Model):
+        # ...
+
+    class Pizza(models.Model):
+        # ...
+        toppings = models.ManyToManyField(Topping)
+
+If we would do something like this:
+
+.. code-block:: python
+
+    >>> p = Pizza.object.create(...)
+    >>> t = Topping.objects.create(...)
+    >>> p.toppings.add(t)
+	
+the arguments sent to a :data:`m2m_changed` handler would be:
+
+    ==============  ============================================================
+    Argument        Value
+    ==============  ============================================================
+    ``sender``      ``Pizza`` (the class containing the field)
+
+    ``instance``    ``p`` (the ``Pizza`` instance being modified)
+
+    ``action``      ``"add"`` since the ``add`` method was called
+    
+    ``model``       ``Topping`` (the class of the objects added to the
+                    ``Pizza``)
+    
+    ``reverse``     ``False`` (since ``Pizza`` contains the 
+                    :class:`ManyToManyField`)
+    
+    ``field_name``  ``"topping"`` (the name of the :class:`ManyToManyField`)
+    
+    ``objects``     ``[t]`` (since only ``Topping t`` was added to the relation)
+    ==============  ============================================================
+
+And if we would then do something like this:
+
+.. code-block:: python
+
+    >>> t.pizza_set.remove(p)
+	
+the arguments sent to a :data:`m2m_changed` handler would be:
+
+    ==============  ============================================================
+    Argument        Value
+    ==============  ============================================================
+    ``sender``      ``Pizza`` (the class containing the field)
+
+    ``instance``    ``t`` (the ``Topping`` instance being modified)
+
+    ``action``      ``"remove"`` since the ``remove`` method was called
+    
+    ``model``       ``Pizza`` (the class of the objects removed from the 
+                    ``Topping``)
+    
+    ``reverse``     ``True`` (since ``Pizza`` contains the
+                    :class:`ManyToManyField` but the relation is modified
+                    through ``Topping``)
+    
+    ``field_name``  ``"topping"`` (the name of the :class:`ManyToManyField`)
+    
+    ``objects``     ``[p]`` (since only ``Pizza p`` was removed from the 
+                    relation)
+    ==============  ============================================================
+
 class_prepared
 --------------
 
