Ticket #5390: t5390-r12120.1.diff
File t5390-r12120.1.diff, 13.1 KB (added by , 15 years ago) |
---|
-
django/db/models/fields/related.py
diff -r 891a2658613f django/db/models/fields/related.py
a b 513 513 source_field_name: self._pk_val, 514 514 '%s__in' % target_field_name: new_ids, 515 515 }) 516 vals = set(vals) 517 516 new_ids = new_ids - set(vals) 518 517 # Add the ones that aren't there already 519 for obj_id in (new_ids - vals):518 for obj_id in new_ids: 520 519 self.through._default_manager.using(self.instance._state.db).create(**{ 521 520 '%s_id' % source_field_name: self._pk_val, 522 521 '%s_id' % target_field_name: obj_id, 523 522 }) 523 signals.m2m_changed.send(sender=rel.through, action='add', 524 instance=self.instance, reverse=(rel.to != self.model), 525 model=self.model, pk_set=new_ids) 524 526 525 527 def _remove_items(self, source_field_name, target_field_name, *objs): 526 528 # source_col_name: the PK colname in join_table for the source object … … 541 543 source_field_name: self._pk_val, 542 544 '%s__in' % target_field_name: old_ids 543 545 }).delete() 546 signals.m2m_changed.send(sender=rel.through, action="remove", 547 instance=self.instance, reverse=(rel.to != self.model), 548 model=self.model, pk_set=old_ids) 544 549 545 550 def _clear_items(self, source_field_name): 546 551 # source_col_name: the PK colname in join_table for the source object 552 signals.m2m_changed.send(sender=rel.through, action="clear", 553 instance=self.instance, reverse=(rel.to != self.model), 554 model=self.model, pk_set=None) 547 555 self.through._default_manager.using(self.instance._state.db).filter(**{ 548 556 source_field_name: self._pk_val 549 557 }).delete() … … 593 601 manager.clear() 594 602 manager.add(*value) 595 603 604 596 605 class ReverseManyRelatedObjectsDescriptor(object): 597 606 # This class provides the functionality that makes the related-object 598 607 # managers available as attributes on a model class, for fields that have -
django/db/models/signals.py
diff -r 891a2658613f django/db/models/signals.py
a b 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=["action", "instance", "reverse", "model", "pk_set"]) -
docs/ref/signals.txt
diff -r 891a2658613f docs/ref/signals.txt
a b 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 intermediate model class describing the :class:`ManyToManyField`. 189 This class is automatically created when a many-to-many field is 190 defined; it you can access it using the ``through`` attribute on the 191 many-to-many field. 192 193 ``instance`` 194 The instance whose many-to-many relation is updated. This can be an 195 instance of the ``sender``, or of the class the :class:`ManyToManyField` 196 is related to. 197 198 ``action`` 199 A string indicating the type of update that is done on the relation. 200 This can be one of the following: 201 202 ``"add"`` 203 Sent *after* one or more objects are added to the relation 204 ``"remove"`` 205 Sent *after* one or more objects are removed from the relation 206 ``"clear"`` 207 Sent *before* the relation is cleared 208 209 ``reverse`` 210 Indicates which side of the relation is updated (i.e., if it is the 211 forward or reverse relation that is being modified). 212 213 ``model`` 214 The class of the objects that are added to, removed from or cleared 215 from the relation. 216 217 ``pk_set`` 218 With the ``"add"`` and ``"remove"`` action, this is a list of 219 primary key values that have been added to or removed from the relation. 220 221 For the ``"clear"`` action, this is ``None``. 222 223 For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled 224 like this: 225 226 .. code-block:: python 227 228 class Topping(models.Model): 229 # ... 230 231 class Pizza(models.Model): 232 # ... 233 toppings = models.ManyToManyField(Topping) 234 235 If we would do something like this: 236 237 .. code-block:: python 238 239 >>> p = Pizza.object.create(...) 240 >>> t = Topping.objects.create(...) 241 >>> p.toppings.add(t) 242 243 the arguments sent to a :data:`m2m_changed` handler would be: 244 245 ============== ============================================================ 246 Argument Value 247 ============== ============================================================ 248 ``sender`` ``Pizza.toppings.through`` (the intermediate m2m class) 249 250 ``instance`` ``p`` (the ``Pizza`` instance being modified) 251 252 ``action`` ``"add"`` 253 254 ``reverse`` ``False`` (``Pizza`` contains the :class:`ManyToManyField`, 255 so this call modifies the forward relation) 256 257 ``model`` ``Topping`` (the class of the objects added to the 258 ``Pizza``) 259 260 ``pk_set`` ``[t.id]`` (since only ``Topping t`` was added to the relation) 261 ============== ============================================================ 262 263 And if we would then do something like this: 264 265 .. code-block:: python 266 267 >>> t.pizza_set.remove(p) 268 269 the arguments sent to a :data:`m2m_changed` handler would be: 270 271 ============== ============================================================ 272 Argument Value 273 ============== ============================================================ 274 ``sender`` ``Pizza.toppings.through`` (the intermediate m2m class) 275 276 ``instance`` ``t`` (the ``Topping`` instance being modified) 277 278 ``action`` ``"remove"`` 279 280 ``reverse`` ``True`` (``Pizza`` contains the :class:`ManyToManyField`, 281 so this call modifies the reverse relation) 282 283 ``model`` ``Pizza`` (the class of the objects removed from the 284 ``Topping``) 285 286 ``pk_set`` ``[p.id]`` (since only ``Pizza p`` was removed from the 287 relation) 288 ============== ============================================================ 289 173 290 class_prepared 174 291 -------------- 175 292 -
docs/topics/signals.txt
diff -r 891a2658613f docs/topics/signals.txt
a b 29 29 Sent before or after a model's :meth:`~django.db.models.Model.delete` 30 30 method is called. 31 31 32 * :data:`django.db.models.signals.m2m_changed` 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` -
new file tests/modeltests/m2m_signals/__init__.py
diff -r 891a2658613f tests/modeltests/m2m_signals/__init__.py
- + 1 -
new file tests/modeltests/m2m_signals/models.py
diff -r 891a2658613f 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 class SportsCar(Car): 22 price = models.IntegerField(max_length=20) 23 24 def m2m_changed_test(signal, sender, **kwargs): 25 print 'm2m_changed signal' 26 print 'instance:', kwargs['instance'] 27 print 'action:', kwargs['action'] 28 print 'reverse:', kwargs['reverse'] 29 print 'model:', kwargs['model'] 30 if kwargs['pk_set']: 31 print 'objects:',kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) 32 33 34 __test__ = {'API_TESTS':""" 35 # Install a listener on one of the two m2m relations. 36 >>> models.signals.m2m_changed.connect(m2m_changed_test, Car.optional_parts.through) 37 38 # Test the add, remove and clear methods on both sides of the 39 # many-to-many relation 40 41 >>> c1 = Car.objects.create(name='VW') 42 >>> c2 = Car.objects.create(name='BMW') 43 >>> c3 = Car.objects.create(name='Toyota') 44 >>> p1 = Part.objects.create(name='Wheelset') 45 >>> p2 = Part.objects.create(name='Doors') 46 >>> p3 = Part.objects.create(name='Engine') 47 >>> p4 = Part.objects.create(name='Airbag') 48 >>> p5 = Part.objects.create(name='Sunroof') 49 50 # adding some default parts to our car - no signal listener installed 51 >>> c1.default_parts.add(p4, p5) 52 53 # Now install a listener 54 >>> models.signals.m2m_changed.connect(m2m_changed_test, Car.default_parts.through) 55 56 >>> c1.default_parts.add(p1, p2, p3) 57 m2m_changed signal 58 instance: VW 59 action: add 60 reverse: False 61 model: <class 'modeltests.m2m_signals.models.Part'> 62 objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>] 63 64 # give the BMW and Toyata some doors as well 65 >>> p2.car_set.add(c2, c3) 66 m2m_changed signal 67 instance: Doors 68 action: add 69 reverse: True 70 model: <class 'modeltests.m2m_signals.models.Car'> 71 objects: [<Car: BMW>, <Car: Toyota>] 72 73 # remove the engine from the VW and the airbag (which is not set but is returned) 74 >>> c1.default_parts.remove(p3, p4) 75 m2m_changed signal 76 instance: VW 77 action: remove 78 reverse: False 79 model: <class 'modeltests.m2m_signals.models.Part'> 80 objects: [<Part: Engine>, <Part: Airbag>] 81 82 # give the VW some optional parts (second relation to same model) 83 >>> c1.optional_parts.add(p4,p5) 84 m2m_changed signal 85 instance: VW 86 action: add 87 reverse: False 88 model: <class 'modeltests.m2m_signals.models.Part'> 89 objects: [<Part: Airbag>, <Part: Sunroof>] 90 91 # add airbag to all the cars (even though the VW already has one) 92 >>> p4.cars_optional.add(c1, c2, c3) 93 m2m_changed signal 94 instance: Airbag 95 action: add 96 reverse: True 97 model: <class 'modeltests.m2m_signals.models.Car'> 98 objects: [<Car: BMW>, <Car: Toyota>] 99 100 # remove airbag from the VW (reverse relation with custom related_name) 101 >>> p4.cars_optional.remove(c1) 102 m2m_changed signal 103 instance: Airbag 104 action: remove 105 reverse: True 106 model: <class 'modeltests.m2m_signals.models.Car'> 107 objects: [<Car: VW>] 108 109 # clear all parts of the VW 110 >>> c1.default_parts.clear() 111 m2m_changed signal 112 instance: VW 113 action: clear 114 reverse: False 115 model: <class 'modeltests.m2m_signals.models.Part'> 116 117 # take all the doors off of cars 118 >>> p2.car_set.clear() 119 m2m_changed signal 120 instance: Doors 121 action: clear 122 reverse: True 123 model: <class 'modeltests.m2m_signals.models.Car'> 124 125 # take all the airbags off of cars (clear reverse relation with custom related_name) 126 >>> p4.cars_optional.clear() 127 m2m_changed signal 128 instance: Airbag 129 action: clear 130 reverse: True 131 model: <class 'modeltests.m2m_signals.models.Car'> 132 133 # alternative ways of setting relation: 134 135 >>> c1.default_parts.create(name='Windows') 136 m2m_changed signal 137 instance: VW 138 action: add 139 reverse: False 140 model: <class 'modeltests.m2m_signals.models.Part'> 141 objects: [<Part: Windows>] 142 <Part: Windows> 143 144 # direct assignment clears the set first, then adds 145 >>> c1.default_parts = [p1,p2,p3] 146 m2m_changed signal 147 instance: VW 148 action: clear 149 reverse: False 150 model: <class 'modeltests.m2m_signals.models.Part'> 151 m2m_changed signal 152 instance: VW 153 action: add 154 reverse: False 155 model: <class 'modeltests.m2m_signals.models.Part'> 156 objects: [<Part: Wheelset>, <Part: Doors>, <Part: Engine>] 157 158 # Check that signals still work when model inheritance is involved 159 >>> c4 = SportsCar.objects.create(name='Bugatti', price='1000000') 160 >>> c4.default_parts = [p2] 161 m2m_changed signal 162 instance: Bugatti 163 action: clear 164 reverse: False 165 model: <class 'modeltests.m2m_signals.models.Part'> 166 m2m_changed signal 167 instance: Bugatti 168 action: add 169 reverse: False 170 model: <class 'modeltests.m2m_signals.models.Part'> 171 objects: [<Part: Doors>] 172 173 >>> p3.car_set.add(c4) 174 m2m_changed signal 175 instance: Engine 176 action: add 177 reverse: True 178 model: <class 'modeltests.m2m_signals.models.Car'> 179 objects: [<Car: Bugatti>] 180 181 >>> models.signals.m2m_changed.disconnect(m2m_changed_test) 182 183 """}