Ticket #5390: complete-patch.diff
File complete-patch.diff, 16.7 KB (added by , 16 years ago) |
---|
-
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"]) -
django/db/models/fields/related.py
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
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
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
Property changes on: tests/modeltests/m2m_signals/__init__.py ___________________________________________________________________ Added: svn:keywords + Id Added: svn:eol-style + native
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
Property changes on: tests/modeltests/m2m_signals/models.py ___________________________________________________________________ Added: svn:keywords + Id Added: svn:eol-style + native
1