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
This commit is contained in:
parent
f56f6e9405
commit
6afd505b5b
|
@ -427,7 +427,8 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
through = rel.through
|
through = rel.through
|
||||||
class ManyRelatedManager(superclass):
|
class ManyRelatedManager(superclass):
|
||||||
def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
|
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__()
|
super(ManyRelatedManager, self).__init__()
|
||||||
self.core_filters = core_filters
|
self.core_filters = core_filters
|
||||||
self.model = model
|
self.model = model
|
||||||
|
@ -437,6 +438,7 @@ def create_many_related_manager(superclass, rel=False):
|
||||||
self.target_field_name = target_field_name
|
self.target_field_name = target_field_name
|
||||||
self.through = through
|
self.through = through
|
||||||
self._pk_val = self.instance.pk
|
self._pk_val = self.instance.pk
|
||||||
|
self.reverse = reverse
|
||||||
if self._pk_val is None:
|
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__)
|
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,
|
source_field_name: self._pk_val,
|
||||||
'%s__in' % target_field_name: new_ids,
|
'%s__in' % target_field_name: new_ids,
|
||||||
})
|
})
|
||||||
vals = set(vals)
|
new_ids = new_ids - set(vals)
|
||||||
|
|
||||||
# Add the ones that aren't there already
|
# 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(**{
|
self.through._default_manager.using(self.instance._state.db).create(**{
|
||||||
'%s_id' % source_field_name: self._pk_val,
|
'%s_id' % source_field_name: self._pk_val,
|
||||||
'%s_id' % target_field_name: obj_id,
|
'%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):
|
def _remove_items(self, source_field_name, target_field_name, *objs):
|
||||||
# source_col_name: the PK colname in join_table for the source object
|
# 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,
|
source_field_name: self._pk_val,
|
||||||
'%s__in' % target_field_name: old_ids
|
'%s__in' % target_field_name: old_ids
|
||||||
}).delete()
|
}).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):
|
def _clear_items(self, source_field_name):
|
||||||
# source_col_name: the PK colname in join_table for the source object
|
# 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(**{
|
self.through._default_manager.using(self.instance._state.db).filter(**{
|
||||||
source_field_name: self._pk_val
|
source_field_name: self._pk_val
|
||||||
}).delete()
|
}).delete()
|
||||||
|
@ -579,7 +598,8 @@ class ManyRelatedObjectsDescriptor(object):
|
||||||
instance=instance,
|
instance=instance,
|
||||||
symmetrical=False,
|
symmetrical=False,
|
||||||
source_field_name=self.related.field.m2m_reverse_field_name(),
|
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
|
return manager
|
||||||
|
@ -596,6 +616,7 @@ class ManyRelatedObjectsDescriptor(object):
|
||||||
manager.clear()
|
manager.clear()
|
||||||
manager.add(*value)
|
manager.add(*value)
|
||||||
|
|
||||||
|
|
||||||
class ReverseManyRelatedObjectsDescriptor(object):
|
class ReverseManyRelatedObjectsDescriptor(object):
|
||||||
# This class provides the functionality that makes the related-object
|
# This class provides the functionality that makes the related-object
|
||||||
# managers available as attributes on a model class, for fields that have
|
# managers available as attributes on a model class, for fields that have
|
||||||
|
@ -629,7 +650,8 @@ class ReverseManyRelatedObjectsDescriptor(object):
|
||||||
instance=instance,
|
instance=instance,
|
||||||
symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
|
symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
|
||||||
source_field_name=self.field.m2m_field_name(),
|
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
|
return manager
|
||||||
|
|
|
@ -12,3 +12,5 @@ pre_delete = Signal(providing_args=["instance"])
|
||||||
post_delete = Signal(providing_args=["instance"])
|
post_delete = Signal(providing_args=["instance"])
|
||||||
|
|
||||||
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
|
post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
|
||||||
|
|
||||||
|
m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set"])
|
||||||
|
|
|
@ -170,6 +170,123 @@ Arguments sent with this signal:
|
||||||
Note that the object will no longer be in the database, so be very
|
Note that the object will no longer be in the database, so be very
|
||||||
careful what you do with this instance.
|
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
|
class_prepared
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,9 @@ notifications:
|
||||||
Sent before or after a model's :meth:`~django.db.models.Model.delete`
|
Sent before or after a model's :meth:`~django.db.models.Model.delete`
|
||||||
method is called.
|
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_started` &
|
||||||
:data:`django.core.signals.request_finished`
|
:data:`django.core.signals.request_finished`
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,252 @@
|
||||||
|
"""
|
||||||
|
Testing signals emitted on changing m2m relations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Part(models.Model):
|
||||||
|
name = models.CharField(max_length=20)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class SportsCar(Car):
|
||||||
|
price = models.IntegerField()
|
||||||
|
|
||||||
|
class Person(models.Model):
|
||||||
|
name = models.CharField(max_length=20)
|
||||||
|
fans = models.ManyToManyField('self', related_name='idols', symmetrical=False)
|
||||||
|
friends = models.ManyToManyField('self')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('name',)
|
||||||
|
|
||||||
|
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 'model:', kwargs['model']
|
||||||
|
if kwargs['pk_set']:
|
||||||
|
print 'objects:',kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||||
|
|
||||||
|
|
||||||
|
__test__ = {'API_TESTS':"""
|
||||||
|
# Install a listener on one of the two m2m relations.
|
||||||
|
>>> models.signals.m2m_changed.connect(m2m_changed_test, Car.optional_parts.through)
|
||||||
|
|
||||||
|
# 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 a default part to our car - no signal listener installed
|
||||||
|
>>> c1.default_parts.add(p5)
|
||||||
|
|
||||||
|
# Now install a listener
|
||||||
|
>>> models.signals.m2m_changed.connect(m2m_changed_test, Car.default_parts.through)
|
||||||
|
|
||||||
|
>>> c1.default_parts.add(p1, p2, p3)
|
||||||
|
m2m_changed signal
|
||||||
|
instance: VW
|
||||||
|
action: add
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Part'>
|
||||||
|
objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
|
||||||
|
|
||||||
|
# give the BMW and Toyata some doors as well
|
||||||
|
>>> p2.car_set.add(c2, c3)
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Doors
|
||||||
|
action: add
|
||||||
|
reverse: True
|
||||||
|
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
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Part'>
|
||||||
|
objects: [<Part: Airbag>, <Part: Engine>]
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Part'>
|
||||||
|
|
||||||
|
# take all the doors off of cars
|
||||||
|
>>> p2.car_set.clear()
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Doors
|
||||||
|
action: clear
|
||||||
|
reverse: True
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Car'>
|
||||||
|
|
||||||
|
# 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
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Car'>
|
||||||
|
|
||||||
|
# alternative ways of setting relation:
|
||||||
|
|
||||||
|
>>> c1.default_parts.create(name='Windows')
|
||||||
|
m2m_changed signal
|
||||||
|
instance: VW
|
||||||
|
action: add
|
||||||
|
reverse: False
|
||||||
|
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
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Part'>
|
||||||
|
m2m_changed signal
|
||||||
|
instance: VW
|
||||||
|
action: add
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Part'>
|
||||||
|
objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
|
||||||
|
|
||||||
|
# Check that signals still work when model inheritance is involved
|
||||||
|
>>> c4 = SportsCar.objects.create(name='Bugatti', price='1000000')
|
||||||
|
>>> c4.default_parts = [p2]
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Bugatti
|
||||||
|
action: clear
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Part'>
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Bugatti
|
||||||
|
action: add
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Part'>
|
||||||
|
objects: [<Part: Doors>]
|
||||||
|
|
||||||
|
>>> p3.car_set.add(c4)
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Engine
|
||||||
|
action: add
|
||||||
|
reverse: True
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Car'>
|
||||||
|
objects: [<Car: Bugatti>]
|
||||||
|
|
||||||
|
# Now test m2m relations with self
|
||||||
|
>>> p1 = Person.objects.create(name='Alice')
|
||||||
|
>>> p2 = Person.objects.create(name='Bob')
|
||||||
|
>>> p3 = Person.objects.create(name='Chuck')
|
||||||
|
>>> p4 = Person.objects.create(name='Daisy')
|
||||||
|
|
||||||
|
>>> models.signals.m2m_changed.connect(m2m_changed_test, Person.fans.through)
|
||||||
|
>>> models.signals.m2m_changed.connect(m2m_changed_test, Person.friends.through)
|
||||||
|
|
||||||
|
>>> p1.friends = [p2, p3]
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Alice
|
||||||
|
action: clear
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Person'>
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Alice
|
||||||
|
action: add
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Person'>
|
||||||
|
objects: [<Person: Bob>, <Person: Chuck>]
|
||||||
|
|
||||||
|
>>> p1.fans = [p4]
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Alice
|
||||||
|
action: clear
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Person'>
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Alice
|
||||||
|
action: add
|
||||||
|
reverse: False
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Person'>
|
||||||
|
objects: [<Person: Daisy>]
|
||||||
|
|
||||||
|
>>> p3.idols = [p1,p2]
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Chuck
|
||||||
|
action: clear
|
||||||
|
reverse: True
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Person'>
|
||||||
|
m2m_changed signal
|
||||||
|
instance: Chuck
|
||||||
|
action: add
|
||||||
|
reverse: True
|
||||||
|
model: <class 'modeltests.m2m_signals.models.Person'>
|
||||||
|
objects: [<Person: Alice>, <Person: Bob>]
|
||||||
|
|
||||||
|
# Cleanup - disconnect all signal handlers
|
||||||
|
>>> models.signals.m2m_changed.disconnect(m2m_changed_test, Car.default_parts.through)
|
||||||
|
>>> models.signals.m2m_changed.disconnect(m2m_changed_test, Car.optional_parts.through)
|
||||||
|
>>> models.signals.m2m_changed.disconnect(m2m_changed_test, Person.fans.through)
|
||||||
|
>>> models.signals.m2m_changed.disconnect(m2m_changed_test, Person.friends.through)
|
||||||
|
|
||||||
|
"""}
|
Loading…
Reference in New Issue