Ticket #5390: 5390_against_11745.diff
File 5390_against_11745.diff, 15.8 KB (added by , 15 years ago) |
---|
-
django/db/models/fields/related.py
411 411 through = rel.through 412 412 class ManyRelatedManager(superclass): 413 413 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): 415 416 super(ManyRelatedManager, self).__init__() 416 417 self.core_filters = core_filters 417 418 self.model = model … … 421 422 self.target_field_name = target_field_name 422 423 self.through = through 423 424 self._pk_val = self.instance.pk 425 self.field_name = field_name 426 self.reverse = reverse 424 427 if self._pk_val is None: 425 428 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) 426 429 … … 497 500 source_field_name: self._pk_val, 498 501 '%s__in' % target_field_name: new_ids, 499 502 }) 500 vals = set(vals) 501 503 new_ids=new_ids-set(vals) 502 504 # Add the ones that aren't there already 503 for obj_id in (new_ids - vals):505 for obj_id in new_ids: 504 506 self.through._default_manager.create(**{ 505 507 '%s_id' % source_field_name: self._pk_val, 506 508 '%s_id' % target_field_name: obj_id, 507 509 }) 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] 508 513 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 509 526 def _remove_items(self, source_field_name, target_field_name, *objs): 510 527 # source_col_name: the PK colname in join_table for the source object 511 528 # target_col_name: the PK colname in join_table for the target object … … 525 542 source_field_name: self._pk_val, 526 543 '%s__in' % target_field_name: old_ids 527 544 }).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)) 528 553 529 554 def _clear_items(self, source_field_name): 530 555 # 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) 531 563 self.through._default_manager.filter(**{ 532 564 source_field_name: self._pk_val 533 565 }).delete() … … 560 592 instance=instance, 561 593 symmetrical=False, 562 594 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 564 598 ) 565 599 566 600 return manager … … 610 644 instance=instance, 611 645 symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)), 612 646 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 614 650 ) 615 651 616 652 return manager -
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