Ticket #5390: 5390_against_11745.diff

File 5390_against_11745.diff, 15.8 KB (added by frans, 6 years ago)

thks Ludovico, this time with the tests

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

     
    411411    through = rel.through
    412412    class ManyRelatedManager(superclass):
    413413        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
    414                 join_table=None, source_field_name=None, target_field_name=None):
     414                join_table=None, source_field_name=None, target_field_name=None,
     415                field_name=None, reverse=False):
    415416            super(ManyRelatedManager, self).__init__()
    416417            self.core_filters = core_filters
    417418            self.model = model
     
    421422            self.target_field_name = target_field_name
    422423            self.through = through
    423424            self._pk_val = self.instance.pk
     425            self.field_name = field_name
     426            self.reverse = reverse
    424427            if self._pk_val is None:
    425428                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
    426429
     
    497500                    source_field_name: self._pk_val,
    498501                    '%s__in' % target_field_name: new_ids,
    499502                })
    500                 vals = set(vals)
    501 
     503                new_ids=new_ids-set(vals)
    502504                # Add the ones that aren't there already
    503                 for obj_id in (new_ids - vals):
     505                for obj_id in new_ids:
    504506                    self.through._default_manager.create(**{
    505507                        '%s_id' % source_field_name: self._pk_val,
    506508                        '%s_id' % target_field_name: obj_id,
    507509                    })
     510                added_objs = [obj for obj in objs if \
     511                      (isinstance(obj, self.model) and obj._get_pk_val() in new_ids) \
     512                      or obj in new_ids]
    508513
     514                if self.reverse:
     515                    sender = self.model
     516                else:
     517                    sender = self.instance.__class__
     518                    signals.m2m_changed.send(sender=sender, action='add',
     519                        instance=self.instance, model=self.model,
     520                        reverse=self.reverse, field_name=self.field_name,
     521                        objects=added_objs)
     522
     523
     524
     525
    509526        def _remove_items(self, source_field_name, target_field_name, *objs):
    510527            # source_col_name: the PK colname in join_table for the source object
    511528            # target_col_name: the PK colname in join_table for the target object
     
    525542                    source_field_name: self._pk_val,
    526543                    '%s__in' % target_field_name: old_ids
    527544                }).delete()
     545                if self.reverse:
     546                    sender = self.model
     547                else:
     548                    sender = self.instance.__class__
     549                signals.m2m_changed.send(sender=sender, action="remove",
     550                    instance=self.instance, model=self.model,
     551                    reverse=self.reverse, field_name=self.field_name, 
     552                    objects=list(objs))
    528553
    529554        def _clear_items(self, source_field_name):
    530555            # source_col_name: the PK colname in join_table for the source object
     556            if self.reverse:
     557                sender = self.model
     558            else:
     559                sender = self.instance.__class__
     560            signals.m2m_changed.send(sender=sender, action="clear",
     561                instance=self.instance, model=self.model, reverse=self.reverse,
     562                field_name=self.field_name, objects=None)
    531563            self.through._default_manager.filter(**{
    532564                source_field_name: self._pk_val
    533565            }).delete()
     
    560592            instance=instance,
    561593            symmetrical=False,
    562594            source_field_name=self.related.field.m2m_reverse_field_name(),
    563             target_field_name=self.related.field.m2m_field_name()
     595            target_field_name=self.related.field.m2m_field_name(),
     596            field_name=self.related.field.name,
     597            reverse=True
    564598        )
    565599
    566600        return manager
     
    610644            instance=instance,
    611645            symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
    612646            source_field_name=self.field.m2m_field_name(),
    613             target_field_name=self.field.m2m_reverse_field_name()
     647            target_field_name=self.field.m2m_reverse_field_name(),
     648            field_name=self.field.name,
     649            reverse=False
    614650        )
    615651
    616652        return manager
  • django/db/models/signals.py

     
    1212post_delete = Signal(providing_args=["instance"])
    1313
    1414post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
     15
     16m2m_changed = Signal(providing_args=["instance", "action", "model", "field_name", "reverse", "objects"])
  • tests/modeltests/m2m_signals/__init__.py

     
     1
  • 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
     21def m2m_changed_test(signal, sender, **kwargs):
     22    print 'm2m_changed signal'
     23    print 'instance:', kwargs['instance']
     24    print 'action:', kwargs['action']
     25    print 'reverse:', kwargs['reverse']
     26    print 'field_name:', kwargs['field_name']
     27    print 'model:', kwargs['model']
     28    print 'objects:',kwargs['objects']
     29   
     30
     31__test__ = {'API_TESTS':"""
     32>>> models.signals.m2m_changed.connect(m2m_changed_test, Car)
     33
     34# Test the add, remove and clear methods on both sides of the
     35# many-to-many relation
     36
     37>>> c1 = Car.objects.create(name='VW')
     38>>> c2 = Car.objects.create(name='BMW')
     39>>> c3 = Car.objects.create(name='Toyota')
     40>>> p1 = Part.objects.create(name='Wheelset')
     41>>> p2 = Part.objects.create(name='Doors')
     42>>> p3 = Part.objects.create(name='Engine')
     43>>> p4 = Part.objects.create(name='Airbag')
     44>>> p5 = Part.objects.create(name='Sunroof')
     45
     46# adding some default parts to our car
     47>>> c1.default_parts.add(p1, p2, p3)
     48m2m_changed signal
     49instance: VW
     50action: add
     51reverse: False
     52field_name: default_parts
     53model: <class 'modeltests.m2m_signals.models.Part'>
     54objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>]
     55
     56# give the BMW and Toyata some doors as well
     57>>> p2.car_set.add(c2, c3)
     58m2m_changed signal
     59instance: Doors
     60action: add
     61reverse: True
     62field_name: default_parts
     63model: <class 'modeltests.m2m_signals.models.Car'>
     64objects: [<Car: BMW>, <Car: Toyota>]
     65
     66# remove the engine from the VW and the airbag (which is not set but is returned)
     67>>> c1.default_parts.remove(p3, p4)
     68m2m_changed signal
     69instance: VW
     70action: remove
     71reverse: False
     72field_name: default_parts
     73model: <class 'modeltests.m2m_signals.models.Part'>
     74objects: [<Part: Engine>, <Part: Airbag>]
     75
     76# give the VW some optional parts (second relation to same model)
     77>>> c1.optional_parts.add(p4,p5)
     78m2m_changed signal
     79instance: VW
     80action: add
     81reverse: False
     82field_name: optional_parts
     83model: <class 'modeltests.m2m_signals.models.Part'>
     84objects: [<Part: Airbag>, <Part: Sunroof>]
     85
     86# add airbag to all the cars (even though the VW already has one)
     87>>> p4.cars_optional.add(c1, c2, c3)
     88m2m_changed signal
     89instance: Airbag
     90action: add
     91reverse: True
     92field_name: optional_parts
     93model: <class 'modeltests.m2m_signals.models.Car'>
     94objects: [<Car: BMW>, <Car: Toyota>]
     95
     96# remove airbag from the VW (reverse relation with custom related_name)
     97>>> p4.cars_optional.remove(c1)
     98m2m_changed signal
     99instance: Airbag
     100action: remove
     101reverse: True
     102field_name: optional_parts
     103model: <class 'modeltests.m2m_signals.models.Car'>
     104objects: [<Car: VW>]
     105
     106# clear all parts of the VW
     107>>> c1.default_parts.clear()
     108m2m_changed signal
     109instance: VW
     110action: clear
     111reverse: False
     112field_name: default_parts
     113model: <class 'modeltests.m2m_signals.models.Part'>
     114objects: None
     115
     116# take all the doors off of cars
     117>>> p2.car_set.clear()
     118m2m_changed signal
     119instance: Doors
     120action: clear
     121reverse: True
     122field_name: default_parts
     123model: <class 'modeltests.m2m_signals.models.Car'>
     124objects: None
     125
     126# take all the airbags off of cars (clear reverse relation with custom related_name)
     127>>> p4.cars_optional.clear()
     128m2m_changed signal
     129instance: Airbag
     130action: clear
     131reverse: True
     132field_name: optional_parts
     133model: <class 'modeltests.m2m_signals.models.Car'>
     134objects: None
     135
     136# alternative ways of setting relation:
     137
     138>>> c1.default_parts.create(name='Windows')
     139m2m_changed signal
     140instance: VW
     141action: add
     142reverse: False
     143field_name: default_parts
     144model: <class 'modeltests.m2m_signals.models.Part'>
     145objects: [<Part: Windows>]
     146<Part: Windows>
     147
     148# direct assignment clears the set first, then adds
     149>>> c1.default_parts = [p1,p2,p3]
     150m2m_changed signal
     151instance: VW
     152action: clear
     153reverse: False
     154field_name: default_parts
     155model: <class 'modeltests.m2m_signals.models.Part'>
     156objects: None
     157m2m_changed signal
     158instance: VW
     159action: add
     160reverse: False
     161field_name: default_parts
     162model: <class 'modeltests.m2m_signals.models.Part'>
     163objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>]
     164
     165>>> models.signals.m2m_changed.disconnect(m2m_changed_test)
     166"""}
  • docs/topics/signals.txt

     
    2828
    2929      Sent before or after a model's :meth:`~django.db.models.Model.delete`
    3030      method is called.
     31     
     32    * :data:`django.db.models.signals.m2m_changed`
    3133
     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`
  • docs/ref/signals.txt

     
    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 model class containing the :class:`ManyToManyField`.
     189
     190    ``instance``
     191        The instance whose many-to-many relation is updated. This can be an
     192        instance of the ``sender``, or of the class the :class:`ManyToManyField`
     193        is related to.
     194
     195    ``action``
     196        A string indicating the type of update that is done on the relation.
     197        This can be one of the following:
     198       
     199        ``"add"``
     200            Sent *after* one or more objects are added to the relation,
     201        ``"remove"``       
     202            Sent *after* one or more objects are removed from the relation,
     203        ``"clear"``
     204            Sent *before* the relation is cleared.
     205       
     206    ``model``
     207        The class of the objects that are added to, removed from or cleared
     208        from the relation.
     209       
     210    ``field_name``
     211        The name of the :class:`ManyToManyField` in the ``sender`` class.
     212        This can be used to identify which relation has changed
     213        when multiple many-to-many relations to the same model
     214        exist in ``sender``.
     215       
     216    ``reverse``
     217        Indicates which side of the relation is updated.
     218        It is ``False`` for updates on an instance of the ``sender`` and
     219        ``True`` for updates on an instance of the related class.
     220       
     221    ``objects``
     222        With the ``"add"`` and ``"remove"`` action, this is a list of
     223        objects that have been added to or removed from the relation.
     224        The class of these objects is given in the ``model`` argument.
     225       
     226        For the ``"clear"`` action, this is ``None`` .
     227       
     228        Note that if you pass primary keys to the ``add`` or ``remove`` method
     229        of a relation, ``objects`` will contain primary keys, not instances.
     230        Also note that if you pass objects to the ``add`` method that are
     231        already in the relation, they will not be in the ``objects`` list.
     232        (This doesn't apply to ``remove``.)
     233       
     234For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled
     235like this:
     236
     237.. code-block:: python
     238
     239    class Topping(models.Model):
     240        # ...
     241
     242    class Pizza(models.Model):
     243        # ...
     244        toppings = models.ManyToManyField(Topping)
     245
     246If we would do something like this:
     247
     248.. code-block:: python
     249
     250    >>> p = Pizza.object.create(...)
     251    >>> t = Topping.objects.create(...)
     252    >>> p.toppings.add(t)
     253       
     254the arguments sent to a :data:`m2m_changed` handler would be:
     255
     256    ==============  ============================================================
     257    Argument        Value
     258    ==============  ============================================================
     259    ``sender``      ``Pizza`` (the class containing the field)
     260
     261    ``instance``    ``p`` (the ``Pizza`` instance being modified)
     262
     263    ``action``      ``"add"`` since the ``add`` method was called
     264   
     265    ``model``       ``Topping`` (the class of the objects added to the
     266                    ``Pizza``)
     267   
     268    ``reverse``     ``False`` (since ``Pizza`` contains the
     269                    :class:`ManyToManyField`)
     270   
     271    ``field_name``  ``"topping"`` (the name of the :class:`ManyToManyField`)
     272   
     273    ``objects``     ``[t]`` (since only ``Topping t`` was added to the relation)
     274    ==============  ============================================================
     275
     276And if we would then do something like this:
     277
     278.. code-block:: python
     279
     280    >>> t.pizza_set.remove(p)
     281       
     282the arguments sent to a :data:`m2m_changed` handler would be:
     283
     284    ==============  ============================================================
     285    Argument        Value
     286    ==============  ============================================================
     287    ``sender``      ``Pizza`` (the class containing the field)
     288
     289    ``instance``    ``t`` (the ``Topping`` instance being modified)
     290
     291    ``action``      ``"remove"`` since the ``remove`` method was called
     292   
     293    ``model``       ``Pizza`` (the class of the objects removed from the
     294                    ``Topping``)
     295   
     296    ``reverse``     ``True`` (since ``Pizza`` contains the
     297                    :class:`ManyToManyField` but the relation is modified
     298                    through ``Topping``)
     299   
     300    ``field_name``  ``"topping"`` (the name of the :class:`ManyToManyField`)
     301   
     302    ``objects``     ``[p]`` (since only ``Pizza p`` was removed from the
     303                    relation)
     304    ==============  ============================================================
     305
    173306class_prepared
    174307--------------
    175308
Back to Top