Skip to content

Commit 6afd505

Browse files
committed
Fixed #5390 -- Added signals for m2m operations. Thanks to the many people (including, most recently, rvdrijst and frans) that have contributed to this patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@12223 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent f56f6e9 commit 6afd505

File tree

6 files changed

+403
-6
lines changed

6 files changed

+403
-6
lines changed

django/db/models/fields/related.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,8 @@ def create_many_related_manager(superclass, rel=False):
427427
through = rel.through
428428
class ManyRelatedManager(superclass):
429429
def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
430-
join_table=None, source_field_name=None, target_field_name=None):
430+
join_table=None, source_field_name=None, target_field_name=None,
431+
reverse=False):
431432
super(ManyRelatedManager, self).__init__()
432433
self.core_filters = core_filters
433434
self.model = model
@@ -437,6 +438,7 @@ def __init__(self, model=None, core_filters=None, instance=None, symmetrical=Non
437438
self.target_field_name = target_field_name
438439
self.through = through
439440
self._pk_val = self.instance.pk
441+
self.reverse = reverse
440442
if self._pk_val is None:
441443
raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
442444

@@ -516,14 +518,19 @@ def _add_items(self, source_field_name, target_field_name, *objs):
516518
source_field_name: self._pk_val,
517519
'%s__in' % target_field_name: new_ids,
518520
})
519-
vals = set(vals)
520-
521+
new_ids = new_ids - set(vals)
521522
# Add the ones that aren't there already
522-
for obj_id in (new_ids - vals):
523+
for obj_id in new_ids:
523524
self.through._default_manager.using(self.instance._state.db).create(**{
524525
'%s_id' % source_field_name: self._pk_val,
525526
'%s_id' % target_field_name: obj_id,
526527
})
528+
if self.reverse or source_field_name == self.source_field_name:
529+
# Don't send the signal when we are inserting the
530+
# duplicate data row for symmetrical reverse entries.
531+
signals.m2m_changed.send(sender=rel.through, action='add',
532+
instance=self.instance, reverse=self.reverse,
533+
model=self.model, pk_set=new_ids)
527534

528535
def _remove_items(self, source_field_name, target_field_name, *objs):
529536
# source_col_name: the PK colname in join_table for the source object
@@ -544,9 +551,21 @@ def _remove_items(self, source_field_name, target_field_name, *objs):
544551
source_field_name: self._pk_val,
545552
'%s__in' % target_field_name: old_ids
546553
}).delete()
554+
if self.reverse or source_field_name == self.source_field_name:
555+
# Don't send the signal when we are deleting the
556+
# duplicate data row for symmetrical reverse entries.
557+
signals.m2m_changed.send(sender=rel.through, action="remove",
558+
instance=self.instance, reverse=self.reverse,
559+
model=self.model, pk_set=old_ids)
547560

548561
def _clear_items(self, source_field_name):
549562
# source_col_name: the PK colname in join_table for the source object
563+
if self.reverse or source_field_name == self.source_field_name:
564+
# Don't send the signal when we are clearing the
565+
# duplicate data rows for symmetrical reverse entries.
566+
signals.m2m_changed.send(sender=rel.through, action="clear",
567+
instance=self.instance, reverse=self.reverse,
568+
model=self.model, pk_set=None)
550569
self.through._default_manager.using(self.instance._state.db).filter(**{
551570
source_field_name: self._pk_val
552571
}).delete()
@@ -579,7 +598,8 @@ def __get__(self, instance, instance_type=None):
579598
instance=instance,
580599
symmetrical=False,
581600
source_field_name=self.related.field.m2m_reverse_field_name(),
582-
target_field_name=self.related.field.m2m_field_name()
601+
target_field_name=self.related.field.m2m_field_name(),
602+
reverse=True
583603
)
584604

585605
return manager
@@ -596,6 +616,7 @@ def __set__(self, instance, value):
596616
manager.clear()
597617
manager.add(*value)
598618

619+
599620
class ReverseManyRelatedObjectsDescriptor(object):
600621
# This class provides the functionality that makes the related-object
601622
# managers available as attributes on a model class, for fields that have
@@ -629,7 +650,8 @@ def __get__(self, instance, instance_type=None):
629650
instance=instance,
630651
symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
631652
source_field_name=self.field.m2m_field_name(),
632-
target_field_name=self.field.m2m_reverse_field_name()
653+
target_field_name=self.field.m2m_reverse_field_name(),
654+
reverse=False
633655
)
634656

635657
return manager

django/db/models/signals.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@
1212
post_delete = Signal(providing_args=["instance"])
1313

1414
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
15+
16+
m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set"])

docs/ref/signals.txt

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,123 @@ Arguments sent with this signal:
170170
Note that the object will no longer be in the database, so be very
171171
careful what you do with this instance.
172172

173+
m2m_changed
174+
-----------
175+
176+
.. data:: django.db.models.signals.m2m_changed
177+
:module:
178+
179+
Sent when a :class:`ManyToManyField` is changed on a model instance.
180+
Strictly speaking, this is not a model signal since it is sent by the
181+
:class:`ManyToManyField`, but since it complements the
182+
:data:`pre_save`/:data:`post_save` and :data:`pre_delete`/:data:`post_delete`
183+
when it comes to tracking changes to models, it is included here.
184+
185+
Arguments sent with this signal:
186+
187+
``sender``
188+
The intermediate model class describing the :class:`ManyToManyField`.
189+
This class is automatically created when a many-to-many field is
190+
defined; it you can access it using the ``through`` attribute on the
191+
many-to-many field.
192+
193+
``instance``
194+
The instance whose many-to-many relation is updated. This can be an
195+
instance of the ``sender``, or of the class the :class:`ManyToManyField`
196+
is related to.
197+
198+
``action``
199+
A string indicating the type of update that is done on the relation.
200+
This can be one of the following:
201+
202+
``"add"``
203+
Sent *after* one or more objects are added to the relation
204+
``"remove"``
205+
Sent *after* one or more objects are removed from the relation
206+
``"clear"``
207+
Sent *before* the relation is cleared
208+
209+
``reverse``
210+
Indicates which side of the relation is updated (i.e., if it is the
211+
forward or reverse relation that is being modified).
212+
213+
``model``
214+
The class of the objects that are added to, removed from or cleared
215+
from the relation.
216+
217+
``pk_set``
218+
With the ``"add"`` and ``"remove"`` action, this is a list of
219+
primary key values that have been added to or removed from the relation.
220+
221+
For the ``"clear"`` action, this is ``None``.
222+
223+
For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled
224+
like this:
225+
226+
.. code-block:: python
227+
228+
class Topping(models.Model):
229+
# ...
230+
231+
class Pizza(models.Model):
232+
# ...
233+
toppings = models.ManyToManyField(Topping)
234+
235+
If we would do something like this:
236+
237+
.. code-block:: python
238+
239+
>>> p = Pizza.object.create(...)
240+
>>> t = Topping.objects.create(...)
241+
>>> p.toppings.add(t)
242+
243+
the arguments sent to a :data:`m2m_changed` handler would be:
244+
245+
============== ============================================================
246+
Argument Value
247+
============== ============================================================
248+
``sender`` ``Pizza.toppings.through`` (the intermediate m2m class)
249+
250+
``instance`` ``p`` (the ``Pizza`` instance being modified)
251+
252+
``action`` ``"add"``
253+
254+
``reverse`` ``False`` (``Pizza`` contains the :class:`ManyToManyField`,
255+
so this call modifies the forward relation)
256+
257+
``model`` ``Topping`` (the class of the objects added to the
258+
``Pizza``)
259+
260+
``pk_set`` ``[t.id]`` (since only ``Topping t`` was added to the relation)
261+
============== ============================================================
262+
263+
And if we would then do something like this:
264+
265+
.. code-block:: python
266+
267+
>>> t.pizza_set.remove(p)
268+
269+
the arguments sent to a :data:`m2m_changed` handler would be:
270+
271+
============== ============================================================
272+
Argument Value
273+
============== ============================================================
274+
``sender`` ``Pizza.toppings.through`` (the intermediate m2m class)
275+
276+
``instance`` ``t`` (the ``Topping`` instance being modified)
277+
278+
``action`` ``"remove"``
279+
280+
``reverse`` ``True`` (``Pizza`` contains the :class:`ManyToManyField`,
281+
so this call modifies the reverse relation)
282+
283+
``model`` ``Pizza`` (the class of the objects removed from the
284+
``Topping``)
285+
286+
``pk_set`` ``[p.id]`` (since only ``Pizza p`` was removed from the
287+
relation)
288+
============== ============================================================
289+
173290
class_prepared
174291
--------------
175292

docs/topics/signals.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ notifications:
2929
Sent before or after a model's :meth:`~django.db.models.Model.delete`
3030
method is called.
3131

32+
* :data:`django.db.models.signals.m2m_changed`
33+
34+
Sent when a :class:`ManyToManyField` on a model is changed.
3235

3336
* :data:`django.core.signals.request_started` &
3437
:data:`django.core.signals.request_finished`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)