Ticket #5390: complete-patch.diff
| File complete-patch.diff, 16.7 kB (added by rvdrijst, 1 year ago) |
|---|
-
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"]) -
django/db/models/fields/related.py
old new 360 360 """Creates a manager that subclasses 'superclass' (which is a Manager) 361 361 and adds behavior for many-to-many related objects.""" 362 362 class ManyRelatedManager(superclass): 363 def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, 364 join_table=None, source_col_name=None, target_col_name=None): 363 def __init__(self, model=None, core_filters=None, instance=None, 364 symmetrical=None, join_table=None, source_col_name=None, 365 target_col_name=None, field_name=None, reverse=False): 365 366 super(ManyRelatedManager, self).__init__() 366 367 self.core_filters = core_filters 367 368 self.model = model … … 372 373 self.target_col_name = target_col_name 373 374 self.through = through 374 375 self._pk_val = self.instance._get_pk_val() 376 self.field_name = field_name 377 self.reverse = reverse 375 378 if self._pk_val is None: 376 379 raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) 377 380 … … 450 453 existing_ids = set([row[0] for row in cursor.fetchall()]) 451 454 452 455 # Add the ones that aren't there already 453 for obj_id in (new_ids - existing_ids): 456 new_ids = new_ids - existing_ids 457 for obj_id in new_ids: 454 458 cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ 455 459 (self.join_table, source_col_name, target_col_name), 456 460 [self._pk_val, obj_id]) 461 added_objs = [obj for obj in objs if \ 462 (isinstance(obj, self.model) and obj._get_pk_val() in new_ids) \ 463 or obj in new_ids] 464 if self.reverse: 465 sender = self.model 466 else: 467 sender = self.instance.__class__ 468 signals.m2m_changed.send(sender=sender, action='add', 469 instance=self.instance, model=self.model, 470 reverse=self.reverse, field_name=self.field_name, 471 objects=added_objs) 457 472 transaction.commit_unless_managed() 458 473 459 474 def _remove_items(self, source_col_name, target_col_name, *objs): … … 476 491 (self.join_table, source_col_name, 477 492 target_col_name, ",".join(['%s'] * len(old_ids))), 478 493 [self._pk_val] + list(old_ids)) 494 if self.reverse: 495 sender = self.model 496 else: 497 sender = self.instance.__class__ 498 signals.m2m_changed.send(sender=sender, action="remove", 499 instance=self.instance, model=self.model, 500 reverse=self.reverse, field_name=self.field_name, 501 objects=list(objs)) 479 502 transaction.commit_unless_managed() 480 503 481 504 def _clear_items(self, source_col_name): 505 if self.reverse: 506 sender = self.model 507 else: 508 sender = self.instance.__class__ 509 signals.m2m_changed.send(sender=sender, action="clear", 510 instance=self.instance, model=self.model, reverse=self.reverse, 511 field_name=self.field_name, objects=None) 482 512 # source_col_name: the PK colname in join_table for the source object 483 513 cursor = connection.cursor() 484 514 cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ … … 516 546 symmetrical=False, 517 547 join_table=qn(self.related.field.m2m_db_table()), 518 548 source_col_name=qn(self.related.field.m2m_reverse_name()), 519 target_col_name=qn(self.related.field.m2m_column_name()) 549 target_col_name=qn(self.related.field.m2m_column_name()), 550 field_name=self.related.field.name, 551 reverse=True 520 552 ) 521 553 522 554 return manager … … 561 593 symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model), 562 594 join_table=qn(self.field.m2m_db_table()), 563 595 source_col_name=qn(self.field.m2m_column_name()), 564 target_col_name=qn(self.field.m2m_reverse_name()) 596 target_col_name=qn(self.field.m2m_reverse_name()), 597 field_name=self.field.name, 598 reverse=False 565 599 ) 566 600 567 601 return manager … … 923 957 # A ManyToManyField is not represented by a single column, 924 958 # so return None. 925 959 return None 926 -
docs/ref/signals.txt
old new 163 163 Note that the object will no longer be in the database, so be very 164 164 careful what you do with this instance. 165 165 166 m2m_changed 167 ----------- 168 169 .. data:: django.db.models.signals.m2m_changed 170 :module: 171 172 Sent when a :class:`ManyToManyField` is changed on a model instance. 173 Strictly speaking, this is not a model signal since it is sent by the 174 :class:`ManyToManyField`, but since it complements the 175 :data:`pre_save`/:data:`post_save` and :data:`pre_delete`/:data:`post_delete` 176 when it comes to tracking changes to models, it is included here. 177 178 Arguments sent with this signal: 179 180 ``sender`` 181 The model class containing the :class:`ManyToManyField`. 182 183 ``instance`` 184 The instance whose many-to-many relation is updated. This can be an 185 instance of the ``sender``, or of the class the :class:`ManyToManyField` 186 is related to. 187 188 ``action`` 189 A string indicating the type of update that is done on the relation. 190 This can be one of the following: 191 192 ``"add"`` 193 Sent *after* one or more objects are added to the relation, 194 ``"remove"`` 195 Sent *after* one or more objects are removed from the relation, 196 ``"clear"`` 197 Sent *before* the relation is cleared. 198 199 ``model`` 200 The class of the objects that are added to, removed from or cleared 201 from the relation. 202 203 ``field_name`` 204 The name of the :class:`ManyToManyField` in the ``sender`` class. 205 This can be used to identify which relation has changed 206 when multiple many-to-many relations to the same model 207 exist in ``sender``. 208 209 ``reverse`` 210 Indicates which side of the relation is updated. 211 It is ``False`` for updates on an instance of the ``sender`` and 212 ``True`` for updates on an instance of the related class. 213 214 ``objects`` 215 With the ``"add"`` and ``"remove"`` action, this is a list of 216 objects that have been added to or removed from the relation. 217 The class of these objects is given in the ``model`` argument. 218 219 For the ``"clear"`` action, this is ``None`` . 220 221 Note that if you pass primary keys to the ``add`` or ``remove`` method 222 of a relation, ``objects`` will contain primary keys, not instances. 223 Also note that if you pass objects to the ``add`` method that are 224 already in the relation, they will not be in the ``objects`` list. 225 (This doesn't apply to ``remove``.) 226 227 For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled 228 like this: 229 230 .. code-block:: python 231 232 class Topping(models.Model): 233 # ... 234 235 class Pizza(models.Model): 236 # ... 237 toppings = models.ManyToManyField(Topping) 238 239 If we would do something like this: 240 241 .. code-block:: python 242 243 >>> p = Pizza.object.create(...) 244 >>> t = Topping.objects.create(...) 245 >>> p.toppings.add(t) 246 247 the arguments sent to a :data:`m2m_changed` handler would be: 248 249 ============== ============================================================ 250 Argument Value 251 ============== ============================================================ 252 ``sender`` ``Pizza`` (the class containing the field) 253 254 ``instance`` ``p`` (the ``Pizza`` instance being modified) 255 256 ``action`` ``"add"`` since the ``add`` method was called 257 258 ``model`` ``Topping`` (the class of the objects added to the 259 ``Pizza``) 260 261 ``reverse`` ``False`` (since ``Pizza`` contains the 262 :class:`ManyToManyField`) 263 264 ``field_name`` ``"topping"`` (the name of the :class:`ManyToManyField`) 265 266 ``objects`` ``[t]`` (since only ``Topping t`` was added to the relation) 267 ============== ============================================================ 268 269 And if we would then do something like this: 270 271 .. code-block:: python 272 273 >>> t.pizza_set.remove(p) 274 275 the arguments sent to a :data:`m2m_changed` handler would be: 276 277 ============== ============================================================ 278 Argument Value 279 ============== ============================================================ 280 ``sender`` ``Pizza`` (the class containing the field) 281 282 ``instance`` ``t`` (the ``Topping`` instance being modified) 283 284 ``action`` ``"remove"`` since the ``remove`` method was called 285 286 ``model`` ``Pizza`` (the class of the objects removed from the 287 ``Topping``) 288 289 ``reverse`` ``True`` (since ``Pizza`` contains the 290 :class:`ManyToManyField` but the relation is modified 291 through ``Topping``) 292 293 ``field_name`` ``"topping"`` (the name of the :class:`ManyToManyField`) 294 295 ``objects`` ``[p]`` (since only ``Pizza p`` was removed from the 296 relation) 297 ============== ============================================================ 298 166 299 class_prepared 167 300 -------------- 168 301 -
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` -
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 """} -
tests/modeltests/m2m_signals/__init__.py
old new
