Ticket #5390: 5390-against-12033.diff
File 5390-against-12033.diff, 18.0 KB (added by , 15 years ago) |
---|
-
django/db/models/fields/related.py
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
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
1 -
tests/modeltests/m2m_signals/models.py
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
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
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