Browse Source

Fixed #13087 -- Modified m2m signals to provide greater flexibility over exactly when notifications are delivered.

This is a BACKWARDS INCOMPATIBLE CHANGE for anyone using the signal names introduced in r12223.

 * If you were listening to "add", you should now listen to "post_add".
 * If you were listening to "remove", you should now listen to "post_remove".
 * If you were listening to "clear", you should now listen to "pre_clear".

You may also want to examine your code to see whether the "pre_add", "pre_remove" or "post_clear" would be better suited to your application.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12888 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Russell Keith-Magee 15 years ago
parent
commit
273a002544
3 changed files with 170 additions and 27 deletions
  1. 22 3
      django/db/models/fields/related.py
  2. 9 3
      docs/ref/signals.txt
  3. 139 21
      tests/modeltests/m2m_signals/models.py

+ 22 - 3
django/db/models/fields/related.py

@@ -559,6 +559,13 @@ def create_many_related_manager(superclass, rel=False):
                     '%s__in' % target_field_name: new_ids,
                 })
                 new_ids = new_ids - set(vals)
+
+                if self.reverse or source_field_name == self.source_field_name:
+                    # Don't send the signal when we are inserting the
+                    # duplicate data row for symmetrical reverse entries.
+                    signals.m2m_changed.send(sender=rel.through, action='pre_add',
+                        instance=self.instance, reverse=self.reverse,
+                        model=self.model, pk_set=new_ids)
                 # Add the ones that aren't there already
                 for obj_id in new_ids:
                     self.through._default_manager.using(db).create(**{
@@ -568,7 +575,7 @@ def create_many_related_manager(superclass, rel=False):
                 if self.reverse or source_field_name == self.source_field_name:
                     # Don't send the signal when we are inserting the
                     # duplicate data row for symmetrical reverse entries.
-                    signals.m2m_changed.send(sender=rel.through, action='add',
+                    signals.m2m_changed.send(sender=rel.through, action='post_add',
                         instance=self.instance, reverse=self.reverse,
                         model=self.model, pk_set=new_ids)
 
@@ -586,6 +593,12 @@ def create_many_related_manager(superclass, rel=False):
                         old_ids.add(obj.pk)
                     else:
                         old_ids.add(obj)
+                if self.reverse or source_field_name == self.source_field_name:
+                    # Don't send the signal when we are deleting the
+                    # duplicate data row for symmetrical reverse entries.
+                    signals.m2m_changed.send(sender=rel.through, action="pre_remove",
+                        instance=self.instance, reverse=self.reverse,
+                        model=self.model, pk_set=old_ids)
                 # Remove the specified objects from the join table
                 db = router.db_for_write(self.through.__class__, instance=self.instance)
                 self.through._default_manager.using(db).filter(**{
@@ -595,7 +608,7 @@ def create_many_related_manager(superclass, rel=False):
                 if self.reverse or source_field_name == self.source_field_name:
                     # Don't send the signal when we are deleting the
                     # duplicate data row for symmetrical reverse entries.
-                    signals.m2m_changed.send(sender=rel.through, action="remove",
+                    signals.m2m_changed.send(sender=rel.through, action="post_remove",
                         instance=self.instance, reverse=self.reverse,
                         model=self.model, pk_set=old_ids)
 
@@ -604,13 +617,19 @@ def create_many_related_manager(superclass, rel=False):
             if self.reverse or source_field_name == self.source_field_name:
                 # Don't send the signal when we are clearing the
                 # duplicate data rows for symmetrical reverse entries.
-                signals.m2m_changed.send(sender=rel.through, action="clear",
+                signals.m2m_changed.send(sender=rel.through, action="pre_clear",
                     instance=self.instance, reverse=self.reverse,
                     model=self.model, pk_set=None)
             db = router.db_for_write(self.through.__class__, instance=self.instance)
             self.through._default_manager.using(db).filter(**{
                 source_field_name: self._pk_val
             }).delete()
+            if self.reverse or source_field_name == self.source_field_name:
+                # Don't send the signal when we are clearing the
+                # duplicate data rows for symmetrical reverse entries.
+                signals.m2m_changed.send(sender=rel.through, action="post_clear",
+                    instance=self.instance, reverse=self.reverse,
+                    model=self.model, pk_set=None)
 
     return ManyRelatedManager
 

+ 9 - 3
docs/ref/signals.txt

@@ -201,12 +201,18 @@ Arguments sent with this signal:
         A string indicating the type of update that is done on the relation.
         This can be one of the following:
 
-        ``"add"``
+        ``"pre_add"``
+            Sent *before* one or more objects are added to the relation
+        ``"post_add"``
             Sent *after* one or more objects are added to the relation
-        ``"remove"``
+        ``"pre_remove"``
             Sent *after* one or more objects are removed from the relation
-        ``"clear"``
+        ``"post_remove"``
+            Sent *after* one or more objects are removed from the relation
+        ``"pre_clear"``
             Sent *before* the relation is cleared
+        ``"post_clear"``
+            Sent *after* the relation is cleared
 
     ``reverse``
     	Indicates which side of the relation is updated (i.e., if it is the

+ 139 - 21
tests/modeltests/m2m_signals/models.py

@@ -73,7 +73,13 @@ __test__ = {'API_TESTS':"""
 >>> c1.default_parts.add(p1, p2, p3)
 m2m_changed signal
 instance: VW
-action: add
+action: pre_add
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
+m2m_changed signal
+instance: VW
+action: post_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
@@ -82,7 +88,13 @@ objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
 >>> p2.car_set.add(c2, c3)
 m2m_changed signal
 instance: Doors
-action: add
+action: pre_add
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: [<Car: BMW>, <Car: Toyota>]
+m2m_changed signal
+instance: Doors
+action: post_add
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Car'>
 objects: [<Car: BMW>, <Car: Toyota>]
@@ -91,7 +103,13 @@ objects: [<Car: BMW>, <Car: Toyota>]
 >>> c1.default_parts.remove(p3, p4)
 m2m_changed signal
 instance: VW
-action: remove
+action: pre_remove
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Airbag>, <Part: Engine>]
+m2m_changed signal
+instance: VW
+action: post_remove
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 objects: [<Part: Airbag>, <Part: Engine>]
@@ -100,7 +118,13 @@ objects: [<Part: Airbag>, <Part: Engine>]
 >>> c1.optional_parts.add(p4,p5)
 m2m_changed signal
 instance: VW
-action: add
+action: pre_add
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Airbag>, <Part: Sunroof>]
+m2m_changed signal
+instance: VW
+action: post_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 objects: [<Part: Airbag>, <Part: Sunroof>]
@@ -109,7 +133,13 @@ objects: [<Part: Airbag>, <Part: Sunroof>]
 >>> p4.cars_optional.add(c1, c2, c3)
 m2m_changed signal
 instance: Airbag
-action: add
+action: pre_add
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: [<Car: BMW>, <Car: Toyota>]
+m2m_changed signal
+instance: Airbag
+action: post_add
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Car'>
 objects: [<Car: BMW>, <Car: Toyota>]
@@ -118,7 +148,13 @@ objects: [<Car: BMW>, <Car: Toyota>]
 >>> p4.cars_optional.remove(c1)
 m2m_changed signal
 instance: Airbag
-action: remove
+action: pre_remove
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: [<Car: VW>]
+m2m_changed signal
+instance: Airbag
+action: post_remove
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Car'>
 objects: [<Car: VW>]
@@ -127,7 +163,12 @@ objects: [<Car: VW>]
 >>> c1.default_parts.clear()
 m2m_changed signal
 instance: VW
-action: clear
+action: pre_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+m2m_changed signal
+instance: VW
+action: post_clear
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 
@@ -135,7 +176,12 @@ model: <class 'modeltests.m2m_signals.models.Part'>
 >>> p2.car_set.clear()
 m2m_changed signal
 instance: Doors
-action: clear
+action: pre_clear
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Car'>
+m2m_changed signal
+instance: Doors
+action: post_clear
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Car'>
 
@@ -143,7 +189,12 @@ model: <class 'modeltests.m2m_signals.models.Car'>
 >>> p4.cars_optional.clear()
 m2m_changed signal
 instance: Airbag
-action: clear
+action: pre_clear
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Car'>
+m2m_changed signal
+instance: Airbag
+action: post_clear
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Car'>
 
@@ -152,7 +203,13 @@ model: <class 'modeltests.m2m_signals.models.Car'>
 >>> c1.default_parts.create(name='Windows')
 m2m_changed signal
 instance: VW
-action: add
+action: pre_add
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Windows>]
+m2m_changed signal
+instance: VW
+action: post_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 objects: [<Part: Windows>]
@@ -162,12 +219,23 @@ objects: [<Part: Windows>]
 >>> c1.default_parts = [p1,p2,p3]
 m2m_changed signal
 instance: VW
-action: clear
+action: pre_clear
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 m2m_changed signal
 instance: VW
-action: add
+action: post_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+m2m_changed signal
+instance: VW
+action: pre_add
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
+m2m_changed signal
+instance: VW
+action: post_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
@@ -177,12 +245,23 @@ objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
 >>> c4.default_parts = [p2]
 m2m_changed signal
 instance: Bugatti
-action: clear
+action: pre_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+m2m_changed signal
+instance: Bugatti
+action: post_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Part'>
+m2m_changed signal
+instance: Bugatti
+action: pre_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
+objects: [<Part: Doors>]
 m2m_changed signal
 instance: Bugatti
-action: add
+action: post_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Part'>
 objects: [<Part: Doors>]
@@ -190,7 +269,13 @@ objects: [<Part: Doors>]
 >>> p3.car_set.add(c4)
 m2m_changed signal
 instance: Engine
-action: add
+action: pre_add
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Car'>
+objects: [<Car: Bugatti>]
+m2m_changed signal
+instance: Engine
+action: post_add
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Car'>
 objects: [<Car: Bugatti>]
@@ -207,12 +292,23 @@ objects: [<Car: Bugatti>]
 >>> p1.friends = [p2, p3]
 m2m_changed signal
 instance: Alice
-action: clear
+action: pre_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Person'>
+m2m_changed signal
+instance: Alice
+action: post_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Person'>
+m2m_changed signal
+instance: Alice
+action: pre_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Person'>
+objects: [<Person: Bob>, <Person: Chuck>]
 m2m_changed signal
 instance: Alice
-action: add
+action: post_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Person'>
 objects: [<Person: Bob>, <Person: Chuck>]
@@ -220,12 +316,23 @@ objects: [<Person: Bob>, <Person: Chuck>]
 >>> p1.fans = [p4]
 m2m_changed signal
 instance: Alice
-action: clear
+action: pre_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Person'>
+m2m_changed signal
+instance: Alice
+action: post_clear
+reverse: False
+model: <class 'modeltests.m2m_signals.models.Person'>
+m2m_changed signal
+instance: Alice
+action: pre_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Person'>
+objects: [<Person: Daisy>]
 m2m_changed signal
 instance: Alice
-action: add
+action: post_add
 reverse: False
 model: <class 'modeltests.m2m_signals.models.Person'>
 objects: [<Person: Daisy>]
@@ -233,12 +340,23 @@ objects: [<Person: Daisy>]
 >>> p3.idols = [p1,p2]
 m2m_changed signal
 instance: Chuck
-action: clear
+action: pre_clear
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Person'>
+m2m_changed signal
+instance: Chuck
+action: post_clear
+reverse: True
+model: <class 'modeltests.m2m_signals.models.Person'>
+m2m_changed signal
+instance: Chuck
+action: pre_add
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Person'>
+objects: [<Person: Alice>, <Person: Bob>]
 m2m_changed signal
 instance: Chuck
-action: add
+action: post_add
 reverse: True
 model: <class 'modeltests.m2m_signals.models.Person'>
 objects: [<Person: Alice>, <Person: Bob>]