Skip to content

Commit

Permalink
Fixed #5390 -- Added signals for m2m operations. Thanks to the many p…
Browse files Browse the repository at this point in the history
…eople (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
  • Loading branch information
freakboy3742 committed Jan 13, 2010
1 parent f56f6e9 commit 6afd505
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 6 deletions.
34 changes: 28 additions & 6 deletions django/db/models/fields/related.py
Expand Up @@ -427,7 +427,8 @@ def create_many_related_manager(superclass, rel=False):
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,
reverse=False):
super(ManyRelatedManager, self).__init__()
self.core_filters = core_filters
self.model = model
Expand All @@ -437,6 +438,7 @@ def __init__(self, model=None, core_filters=None, instance=None, symmetrical=Non
self.target_field_name = target_field_name
self.through = through
self._pk_val = self.instance.pk
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__)

Expand Down Expand Up @@ -516,14 +518,19 @@ def _add_items(self, source_field_name, target_field_name, *objs):
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.using(self.instance._state.db).create(**{
'%s_id' % source_field_name: self._pk_val,
'%s_id' % target_field_name: obj_id,
})
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',
instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=new_ids)

def _remove_items(self, source_field_name, target_field_name, *objs):
# source_col_name: the PK colname in join_table for the source object
Expand All @@ -544,9 +551,21 @@ def _remove_items(self, source_field_name, target_field_name, *objs):
source_field_name: self._pk_val,
'%s__in' % target_field_name: old_ids
}).delete()
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",
instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=old_ids)

def _clear_items(self, source_field_name):
# source_col_name: the PK colname in join_table for the source object
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",
instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=None)
self.through._default_manager.using(self.instance._state.db).filter(**{
source_field_name: self._pk_val
}).delete()
Expand Down Expand Up @@ -579,7 +598,8 @@ def __get__(self, instance, instance_type=None):
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(),
reverse=True
)

return manager
Expand All @@ -596,6 +616,7 @@ def __set__(self, instance, value):
manager.clear()
manager.add(*value)


class ReverseManyRelatedObjectsDescriptor(object):
# This class provides the functionality that makes the related-object
# managers available as attributes on a model class, for fields that have
Expand Down Expand Up @@ -629,7 +650,8 @@ def __get__(self, instance, instance_type=None):
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(),
reverse=False
)

return manager
Expand Down
2 changes: 2 additions & 0 deletions django/db/models/signals.py
Expand Up @@ -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=["action", "instance", "reverse", "model", "pk_set"])
117 changes: 117 additions & 0 deletions docs/ref/signals.txt
Expand Up @@ -170,6 +170,123 @@ Arguments sent with this signal:
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 intermediate model class describing the :class:`ManyToManyField`.
This class is automatically created when a many-to-many field is
defined; it you can access it using the ``through`` attribute on the
many-to-many field.

``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

``reverse``
Indicates which side of the relation is updated (i.e., if it is the
forward or reverse relation that is being modified).

``model``
The class of the objects that are added to, removed from or cleared
from the relation.

``pk_set``
With the ``"add"`` and ``"remove"`` action, this is a list of
primary key values that have been added to or removed from the relation.

For the ``"clear"`` action, this is ``None``.

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.toppings.through`` (the intermediate m2m class)

``instance`` ``p`` (the ``Pizza`` instance being modified)

``action`` ``"add"``

``reverse`` ``False`` (``Pizza`` contains the :class:`ManyToManyField`,
so this call modifies the forward relation)

``model`` ``Topping`` (the class of the objects added to the
``Pizza``)

``pk_set`` ``[t.id]`` (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.toppings.through`` (the intermediate m2m class)

``instance`` ``t`` (the ``Topping`` instance being modified)

``action`` ``"remove"``

``reverse`` ``True`` (``Pizza`` contains the :class:`ManyToManyField`,
so this call modifies the reverse relation)

``model`` ``Pizza`` (the class of the objects removed from the
``Topping``)

``pk_set`` ``[p.id]`` (since only ``Pizza p`` was removed from the
relation)
============== ============================================================

class_prepared
--------------

Expand Down
3 changes: 3 additions & 0 deletions docs/topics/signals.txt
Expand Up @@ -29,6 +29,9 @@ notifications:
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`
Expand Down
1 change: 1 addition & 0 deletions tests/modeltests/m2m_signals/__init__.py
@@ -0,0 +1 @@

0 comments on commit 6afd505

Please sign in to comment.