Ticket #5390: 5390-against-12033.diff
| File 5390-against-12033.diff, 18.0 kB (added by frans, 2 months ago) |
|---|
-
django/db/models/fields/related.py
old new 424 424 through = rel.through 425 425 class ManyRelatedManager(superclass): 426 426 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): 428 429 super(ManyRelatedManager, self).__init__() 429 430 self.core_filters = core_filters 430 431 self.model = model … … 434 435 self.target_field_name = target_field_name 435 436 self.through = through 436 437 self._pk_val = self.instance.pk 438 self.field_name = field_name 439 self.reverse = reverse 437 440 if self._pk_val is None: 438 441 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) 439 442 … … 513 516 source_field_name: self._pk_val, 514 517 '%s__in' % target_field_name: new_ids, 515 518 }) 516 vals = set(vals) 517 519 new_ids = new_ids - set(vals) 518 520 # Add the ones that aren't there already 519 for obj_id in (new_ids - vals):521 for obj_id in new_ids : 520 522 self.through._default_manager.using(self.instance._state.db).create(**{ 521 523 '%s_id' % source_field_name: self._pk_val, 522 524 '%s_id' % target_field_name: obj_id, 523 525 }) 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) 524 537 525 538 def _remove_items(self, source_field_name, target_field_name, *objs): 526 539 # source_col_name: the PK colname in join_table for the source object … … 541 554 source_field_name: self._pk_val, 542 555 '%s__in' % target_field_name: old_ids 543 556 }).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)) 544 565 566 545 567 def _clear_items(self, source_field_name): 546 568 # 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) 547 576 self.through._default_manager.using(self.instance._state.db).filter(**{ 548 577 source_field_name: self._pk_val 549 578 }).delete() … … 576 605 instance=instance, 577 606 symmetrical=False, 578 607 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 580 611 ) 581 612 582 613 return manager … … 590 621 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) 591 622 592 623 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) 595 635 636 596 637 class ReverseManyRelatedObjectsDescriptor(object): 597 638 # This class provides the functionality that makes the related-object 598 639 # managers available as attributes on a model class, for fields that have … … 626 667 instance=instance, 627 668 symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)), 628 669 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 630 673 ) 631 674 632 675 return manager … … 640 683 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) 641 684 642 685 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) 645 697 646 698 class ManyToOneRel(object): 647 699 def __init__(self, to, field_name, related_name=None, -
django/db/models/signals.py
old new 12 12 post_delete = Signal(providing_args=["instance"]) 13 13 14 14 post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"]) 15 16 m2m_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 """ 2 Testing signals emitted on changing m2m relations. 3 """ 4 5 from django.db import models 6 7 class Part(models.Model): 8 name = models.CharField(max_length=20) 9 10 def __unicode__(self): 11 return self.name 12 13 class 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 21 def 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) 48 m2m_changed signal 49 instance: VW 50 action: add 51 reverse: False 52 field_name: default_parts 53 model: <class 'modeltests.m2m_signals.models.Part'> 54 objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>] 55 56 # give the BMW and Toyata some doors as well 57 >>> p2.car_set.add(c2, c3) 58 m2m_changed signal 59 instance: Doors 60 action: add 61 reverse: True 62 field_name: default_parts 63 model: <class 'modeltests.m2m_signals.models.Car'> 64 objects: [<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) 68 m2m_changed signal 69 instance: VW 70 action: remove 71 reverse: False 72 field_name: default_parts 73 model: <class 'modeltests.m2m_signals.models.Part'> 74 objects: [<Part: Engine>, <Part: Airbag>] 75 76 # give the VW some optional parts (second relation to same model) 77 >>> c1.optional_parts.add(p4,p5) 78 m2m_changed signal 79 instance: VW 80 action: add 81 reverse: False 82 field_name: optional_parts 83 model: <class 'modeltests.m2m_signals.models.Part'> 84 objects: [<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) 88 m2m_changed signal 89 instance: Airbag 90 action: add 91 reverse: True 92 field_name: optional_parts 93 model: <class 'modeltests.m2m_signals.models.Car'> 94 objects: [<Car: BMW>, <Car: Toyota>] 95 96 # remove airbag from the VW (reverse relation with custom related_name) 97 >>> p4.cars_optional.remove(c1) 98 m2m_changed signal 99 instance: Airbag 100 action: remove 101 reverse: True 102 field_name: optional_parts 103 model: <class 'modeltests.m2m_signals.models.Car'> 104 objects: [<Car: VW>] 105 106 # clear all parts of the VW 107 >>> c1.default_parts.clear() 108 m2m_changed signal 109 instance: VW 110 action: clear 111 reverse: False 112 field_name: default_parts 113 model: <class 'modeltests.m2m_signals.models.Part'> 114 objects: None 115 116 # take all the doors off of cars 117 >>> p2.car_set.clear() 118 m2m_changed signal 119 instance: Doors 120 action: clear 121 reverse: True 122 field_name: default_parts 123 model: <class 'modeltests.m2m_signals.models.Car'> 124 objects: None 125 126 # take all the airbags off of cars (clear reverse relation with custom related_name) 127 >>> p4.cars_optional.clear() 128 m2m_changed signal 129 instance: Airbag 130 action: clear 131 reverse: True 132 field_name: optional_parts 133 model: <class 'modeltests.m2m_signals.models.Car'> 134 objects: None 135 136 # alternative ways of setting relation: 137 138 >>> c1.default_parts.create(name='Windows') 139 m2m_changed signal 140 instance: VW 141 action: add 142 reverse: False 143 field_name: default_parts 144 model: <class 'modeltests.m2m_signals.models.Part'> 145 objects: [<Part: Windows>] 146 <Part: Windows> 147 148 # direct assignment clears the set first, then adds 149 >>> c1.default_parts = [p1,p2,p3] 150 m2m_changed signal 151 instance: VW 152 action: clear 153 reverse: False 154 field_name: default_parts 155 model: <class 'modeltests.m2m_signals.models.Part'> 156 objects: None 157 m2m_changed signal 158 instance: VW 159 action: add 160 reverse: False 161 field_name: default_parts 162 model: <class 'modeltests.m2m_signals.models.Part'> 163 objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>] 164 165 >>> models.signals.m2m_changed.disconnect(m2m_changed_test) 166 """} -
docs/topics/signals.txt
old new 28 28 29 29 Sent before or after a model's :meth:`~django.db.models.Model.delete` 30 30 method is called. 31 32 * :data:`django.db.models.signals.m2m_changed` 31 33 34 Sent when a :class:`ManyToManyField` on a model is changed. 32 35 33 36 * :data:`django.core.signals.request_started` & 34 37 :data:`django.core.signals.request_finished` -
docs/ref/signals.txt
old new 170 170 Note that the object will no longer be in the database, so be very 171 171 careful what you do with this instance. 172 172 173 m2m_changed 174 ----------- 175 176 .. data:: django.db.models.signals.m2m_changed 177 :module: 178 179 Sent when a :class:`ManyToManyField` is changed on a model instance. 180 Strictly 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` 183 when it comes to tracking changes to models, it is included here. 184 185 Arguments 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 234 For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled 235 like 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 246 If 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 254 the 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 276 And if we would then do something like this: 277 278 .. code-block:: python 279 280 >>> t.pizza_set.remove(p) 281 282 the 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 173 306 class_prepared 174 307 -------------- 175 308
