Ticket #5390: t5390-r12120.1.diff

File t5390-r12120.1.diff, 13.1 KB (added by russellm, 5 years ago)

Revised implementation of m2m signals

  • django/db/models/fields/related.py

    diff -r 891a2658613f django/db/models/fields/related.py
    a b  
    513513                    source_field_name: self._pk_val,
    514514                    '%s__in' % target_field_name: new_ids,
    515515                })
    516                 vals = set(vals)
    517 
     516                new_ids = new_ids - set(vals)
    518517                # Add the ones that aren't there already
    519                 for obj_id in (new_ids - vals):
     518                for obj_id in new_ids:
    520519                    self.through._default_manager.using(self.instance._state.db).create(**{
    521520                        '%s_id' % source_field_name: self._pk_val,
    522521                        '%s_id' % target_field_name: obj_id,
    523522                    })
     523                signals.m2m_changed.send(sender=rel.through, action='add',
     524                    instance=self.instance, reverse=(rel.to != self.model),
     525                    model=self.model, pk_set=new_ids)
    524526
    525527        def _remove_items(self, source_field_name, target_field_name, *objs):
    526528            # source_col_name: the PK colname in join_table for the source object
     
    541543                    source_field_name: self._pk_val,
    542544                    '%s__in' % target_field_name: old_ids
    543545                }).delete()
     546                signals.m2m_changed.send(sender=rel.through, action="remove",
     547                    instance=self.instance, reverse=(rel.to != self.model),
     548                    model=self.model, pk_set=old_ids)
    544549
    545550        def _clear_items(self, source_field_name):
    546551            # source_col_name: the PK colname in join_table for the source object
     552            signals.m2m_changed.send(sender=rel.through, action="clear",
     553                instance=self.instance, reverse=(rel.to != self.model),
     554                model=self.model, pk_set=None)
    547555            self.through._default_manager.using(self.instance._state.db).filter(**{
    548556                source_field_name: self._pk_val
    549557            }).delete()
     
    593601        manager.clear()
    594602        manager.add(*value)
    595603
     604
    596605class ReverseManyRelatedObjectsDescriptor(object):
    597606    # This class provides the functionality that makes the related-object
    598607    # managers available as attributes on a model class, for fields that have
  • django/db/models/signals.py

    diff -r 891a2658613f django/db/models/signals.py
    a b  
    1212post_delete = Signal(providing_args=["instance"])
    1313
    1414post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
     15
     16m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set"])
  • docs/ref/signals.txt

    diff -r 891a2658613f docs/ref/signals.txt
    a b  
    170170        Note that the object will no longer be in the database, so be very
    171171        careful what you do with this instance.
    172172
     173m2m_changed
     174-----------
     175
     176.. data:: django.db.models.signals.m2m_changed
     177   :module:
     178
     179Sent when a :class:`ManyToManyField` is changed on a model instance.
     180Strictly 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`
     183when it comes to tracking changes to models, it is included here.
     184
     185Arguments 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
     223For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled
     224like 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
     235If 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
     243the 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
     263And if we would then do something like this:
     264
     265.. code-block:: python
     266
     267    >>> t.pizza_set.remove(p)
     268
     269the 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
    173290class_prepared
    174291--------------
    175292
  • docs/topics/signals.txt

    diff -r 891a2658613f docs/topics/signals.txt
    a b  
    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`
  • new file tests/modeltests/m2m_signals/__init__.py

    diff -r 891a2658613f tests/modeltests/m2m_signals/__init__.py
    - +  
     1
  • new file tests/modeltests/m2m_signals/models.py

    diff -r 891a2658613f tests/modeltests/m2m_signals/models.py
    - +  
     1"""
     2Testing signals emitted on changing m2m relations.
     3"""
     4
     5from django.db import models
     6
     7class Part(models.Model):
     8    name = models.CharField(max_length=20)
     9
     10    def __unicode__(self):
     11        return self.name
     12
     13class Car(models.Model):
     14    name = models.CharField(max_length=20)
     15    default_parts = models.ManyToManyField(Part)
     16    optional_parts = models.ManyToManyField(Part, related_name='cars_optional')
     17
     18    def __unicode__(self):
     19        return self.name
     20
     21class SportsCar(Car):
     22    price = models.IntegerField(max_length=20)
     23
     24def m2m_changed_test(signal, sender, **kwargs):
     25    print 'm2m_changed signal'
     26    print 'instance:', kwargs['instance']
     27    print 'action:', kwargs['action']
     28    print 'reverse:', kwargs['reverse']
     29    print 'model:', kwargs['model']
     30    if kwargs['pk_set']:
     31        print 'objects:',kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
     32
     33
     34__test__ = {'API_TESTS':"""
     35# Install a listener on one of the two m2m relations.
     36>>> models.signals.m2m_changed.connect(m2m_changed_test, Car.optional_parts.through)
     37
     38# Test the add, remove and clear methods on both sides of the
     39# many-to-many relation
     40
     41>>> c1 = Car.objects.create(name='VW')
     42>>> c2 = Car.objects.create(name='BMW')
     43>>> c3 = Car.objects.create(name='Toyota')
     44>>> p1 = Part.objects.create(name='Wheelset')
     45>>> p2 = Part.objects.create(name='Doors')
     46>>> p3 = Part.objects.create(name='Engine')
     47>>> p4 = Part.objects.create(name='Airbag')
     48>>> p5 = Part.objects.create(name='Sunroof')
     49
     50# adding some default parts to our car - no signal listener installed
     51>>> c1.default_parts.add(p4, p5)
     52
     53# Now install a listener
     54>>> models.signals.m2m_changed.connect(m2m_changed_test, Car.default_parts.through)
     55
     56>>> c1.default_parts.add(p1, p2, p3)
     57m2m_changed signal
     58instance: VW
     59action: add
     60reverse: False
     61model: <class 'modeltests.m2m_signals.models.Part'>
     62objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>]
     63
     64# give the BMW and Toyata some doors as well
     65>>> p2.car_set.add(c2, c3)
     66m2m_changed signal
     67instance: Doors
     68action: add
     69reverse: True
     70model: <class 'modeltests.m2m_signals.models.Car'>
     71objects: [<Car: BMW>, <Car: Toyota>]
     72
     73# remove the engine from the VW and the airbag (which is not set but is returned)
     74>>> c1.default_parts.remove(p3, p4)
     75m2m_changed signal
     76instance: VW
     77action: remove
     78reverse: False
     79model: <class 'modeltests.m2m_signals.models.Part'>
     80objects: [<Part: Engine>, <Part: Airbag>]
     81
     82# give the VW some optional parts (second relation to same model)
     83>>> c1.optional_parts.add(p4,p5)
     84m2m_changed signal
     85instance: VW
     86action: add
     87reverse: False
     88model: <class 'modeltests.m2m_signals.models.Part'>
     89objects: [<Part: Airbag>, <Part: Sunroof>]
     90
     91# add airbag to all the cars (even though the VW already has one)
     92>>> p4.cars_optional.add(c1, c2, c3)
     93m2m_changed signal
     94instance: Airbag
     95action: add
     96reverse: True
     97model: <class 'modeltests.m2m_signals.models.Car'>
     98objects: [<Car: BMW>, <Car: Toyota>]
     99
     100# remove airbag from the VW (reverse relation with custom related_name)
     101>>> p4.cars_optional.remove(c1)
     102m2m_changed signal
     103instance: Airbag
     104action: remove
     105reverse: True
     106model: <class 'modeltests.m2m_signals.models.Car'>
     107objects: [<Car: VW>]
     108
     109# clear all parts of the VW
     110>>> c1.default_parts.clear()
     111m2m_changed signal
     112instance: VW
     113action: clear
     114reverse: False
     115model: <class 'modeltests.m2m_signals.models.Part'>
     116
     117# take all the doors off of cars
     118>>> p2.car_set.clear()
     119m2m_changed signal
     120instance: Doors
     121action: clear
     122reverse: True
     123model: <class 'modeltests.m2m_signals.models.Car'>
     124
     125# take all the airbags off of cars (clear reverse relation with custom related_name)
     126>>> p4.cars_optional.clear()
     127m2m_changed signal
     128instance: Airbag
     129action: clear
     130reverse: True
     131model: <class 'modeltests.m2m_signals.models.Car'>
     132
     133# alternative ways of setting relation:
     134
     135>>> c1.default_parts.create(name='Windows')
     136m2m_changed signal
     137instance: VW
     138action: add
     139reverse: False
     140model: <class 'modeltests.m2m_signals.models.Part'>
     141objects: [<Part: Windows>]
     142<Part: Windows>
     143
     144# direct assignment clears the set first, then adds
     145>>> c1.default_parts = [p1,p2,p3]
     146m2m_changed signal
     147instance: VW
     148action: clear
     149reverse: False
     150model: <class 'modeltests.m2m_signals.models.Part'>
     151m2m_changed signal
     152instance: VW
     153action: add
     154reverse: False
     155model: <class 'modeltests.m2m_signals.models.Part'>
     156objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>]
     157
     158# Check that signals still work when model inheritance is involved
     159>>> c4 = SportsCar.objects.create(name='Bugatti', price='1000000')
     160>>> c4.default_parts = [p2]
     161m2m_changed signal
     162instance: Bugatti
     163action: clear
     164reverse: False
     165model: <class 'modeltests.m2m_signals.models.Part'>
     166m2m_changed signal
     167instance: Bugatti
     168action: add
     169reverse: False
     170model: <class 'modeltests.m2m_signals.models.Part'>
     171objects: [<Part: Doors>]
     172
     173>>> p3.car_set.add(c4)
     174m2m_changed signal
     175instance: Engine
     176action: add
     177reverse: True
     178model: <class 'modeltests.m2m_signals.models.Car'>
     179objects: [<Car: Bugatti>]
     180
     181>>> models.signals.m2m_changed.disconnect(m2m_changed_test)
     182
     183"""}
Back to Top