Commit 6afd505b authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #5390 -- Added signals for m2m operations. Thanks to the many people...

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
parent f56f6e94
Loading
Loading
Loading
Loading
+28 −6
Original line number Diff line number Diff line
@@ -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
@@ -437,6 +438,7 @@ def create_many_related_manager(superclass, rel=False):
            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__)

@@ -516,14 +518,19 @@ def create_many_related_manager(superclass, rel=False):
                    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
@@ -544,9 +551,21 @@ def create_many_related_manager(superclass, rel=False):
                    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()
@@ -579,7 +598,8 @@ class ManyRelatedObjectsDescriptor(object):
            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
@@ -596,6 +616,7 @@ class ManyRelatedObjectsDescriptor(object):
        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
@@ -629,7 +650,8 @@ class ReverseManyRelatedObjectsDescriptor(object):
            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
+2 −0
Original line number Diff line number Diff line
@@ -12,3 +12,5 @@ pre_delete = Signal(providing_args=["instance"])
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 −0
Original line number Diff line number Diff line
@@ -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
--------------

+3 −0
Original line number Diff line number Diff line
@@ -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`
+1 −0
Original line number Diff line number Diff line
Loading