Django

Code

Ticket #5390: t5390-r12120.1.diff

File t5390-r12120.1.diff, 13.1 kB (added by russellm, 2 months ago)

Revised implementation of m2m signals

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

    old new  
    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 
  • a/django/db/models/signals.py

    old new  
    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"]) 
  • a/docs/ref/signals.txt

    old new  
    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 
  • a/docs/topics/signals.txt

    old new  
    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` 
  • /dev/null

    old new  
     1 
  • /dev/null

    old new  
     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"""}