Django

Code

Ticket #5390: 5390-against-12033.diff

File 5390-against-12033.diff, 18.0 kB (added by frans, 2 months ago)

updated diff with correct clear() management and more recent version of trunk

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

    old new  
    424424    through = rel.through 
    425425    class ManyRelatedManager(superclass): 
    426426        def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 
    427                 join_table=None, source_field_name=None, target_field_name=None): 
     427                join_table=None, source_field_name=None, target_field_name=None, 
     428                field_name=None, reverse=False): 
    428429            super(ManyRelatedManager, self).__init__() 
    429430            self.core_filters = core_filters 
    430431            self.model = model 
     
    434435            self.target_field_name = target_field_name 
    435436            self.through = through 
    436437            self._pk_val = self.instance.pk 
     438            self.field_name = field_name 
     439            self.reverse = reverse 
    437440            if self._pk_val is None: 
    438441                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) 
    439442 
     
    513516                    source_field_name: self._pk_val, 
    514517                    '%s__in' % target_field_name: new_ids, 
    515518                }) 
    516                 vals = set(vals) 
    517  
     519                new_ids = new_ids - set(vals) 
    518520                # Add the ones that aren't there already 
    519                 for obj_id in (new_ids - vals)
     521                for obj_id in new_ids
    520522                    self.through._default_manager.using(self.instance._state.db).create(**{ 
    521523                        '%s_id' % source_field_name: self._pk_val, 
    522524                        '%s_id' % target_field_name: obj_id, 
    523525                    }) 
     526                added_objs = [obj for obj in objs if \ 
     527                      (isinstance(obj, self.model) and obj._get_pk_val() in new_ids) \ 
     528                      or obj in new_ids] 
     529                if self.reverse: 
     530                    sender = self.model 
     531                else: 
     532                    sender = self.instance.__class__ 
     533                    signals.m2m_changed.send(sender=sender, action='add', 
     534                        instance=self.instance, model=self.model,  
     535                        reverse=self.reverse, field_name=self.field_name,  
     536                        objects=added_objs) 
    524537 
    525538        def _remove_items(self, source_field_name, target_field_name, *objs): 
    526539            # source_col_name: the PK colname in join_table for the source object 
     
    541554                    source_field_name: self._pk_val, 
    542555                    '%s__in' % target_field_name: old_ids 
    543556                }).delete() 
     557                if self.reverse:  
     558                    sender = self.model  
     559                else:  
     560                    sender = self.instance.__class__  
     561                signals.m2m_changed.send(sender=sender, action="remove",  
     562                    instance=self.instance, model=self.model,  
     563                    reverse=self.reverse, field_name=self.field_name,   
     564                    objects=list(objs)) 
    544565 
     566 
    545567        def _clear_items(self, source_field_name): 
    546568            # source_col_name: the PK colname in join_table for the source object 
     569            if self.reverse: 
     570                sender = self.model 
     571            else: 
     572                sender = self.instance.__class__ 
     573            signals.m2m_changed.send(sender=sender, action="clear", 
     574                instance=self.instance, model=self.model, reverse=self.reverse,  
     575                field_name=self.field_name, objects=None) 
    547576            self.through._default_manager.using(self.instance._state.db).filter(**{ 
    548577                source_field_name: self._pk_val 
    549578            }).delete() 
     
    576605            instance=instance, 
    577606            symmetrical=False, 
    578607            source_field_name=self.related.field.m2m_reverse_field_name(), 
    579             target_field_name=self.related.field.m2m_field_name() 
     608            target_field_name=self.related.field.m2m_field_name(), 
     609            field_name=self.related.field.name,  
     610            reverse=True  
    580611        ) 
    581612 
    582613        return manager 
     
    590621            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 
    591622 
    592623        manager = self.__get__(instance) 
    593         manager.clear() 
    594         manager.add(*value) 
     624        previous=set(manager.all()) 
     625        new=set(value) 
     626        if not new: 
     627            manager.clear() 
     628        else: 
     629            added=new-previous 
     630            removed=previous-new 
     631            if added : 
     632                manager.add(*added) 
     633            if removed :  
     634                manager.remove(*removed) 
    595635 
     636 
    596637class ReverseManyRelatedObjectsDescriptor(object): 
    597638    # This class provides the functionality that makes the related-object 
    598639    # managers available as attributes on a model class, for fields that have 
     
    626667            instance=instance, 
    627668            symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)), 
    628669            source_field_name=self.field.m2m_field_name(), 
    629             target_field_name=self.field.m2m_reverse_field_name() 
     670            target_field_name=self.field.m2m_reverse_field_name(), 
     671            field_name=self.field.name,  
     672            reverse=False 
    630673        ) 
    631674 
    632675        return manager 
     
    640683            raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model.  Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) 
    641684 
    642685        manager = self.__get__(instance) 
    643         manager.clear() 
    644         manager.add(*value) 
     686        previous=set(manager.all()) 
     687        new=set(value) 
     688        if not new: 
     689            manager.clear() 
     690        else: 
     691            added=new-previous 
     692            removed=previous-new 
     693            if added : 
     694                manager.add(*added) 
     695            if removed :  
     696                manager.remove(*removed) 
    645697 
    646698class ManyToOneRel(object): 
    647699    def __init__(self, to, field_name, related_name=None, 
  • 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=["instance", "action", "model", "field_name", "reverse", "objects"]) 
  • tests/modeltests/m2m_signals/__init__.py

    old new  
  • tests/modeltests/m2m_signals/models.py

    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 
     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

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

    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 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