Ticket #6707: 6707_m2m_set.diff
File 6707_m2m_set.diff, 10.8 KB (added by , 13 years ago) |
---|
-
docs/ref/signals.txt
332 332 ``using`` ``"default"`` (since the default router sends writes here) 333 333 ============== ============================================================ 334 334 335 We can also assign directly to the relation directly:: 336 337 >>> p.toppings = [t] 338 339 and the :data:`m2m_handler` will receive ``"pre_remove"`` and 340 ``"post_remove"`` actions, if there were ``Topping`` objects to 341 remove. These are followed by ``"pre_add"`` and ``"post_add"`` actions. 342 343 Assigning an empty iterable:: 344 345 >>> p.toppings = [] 346 347 will send ``"pre_clear"`` and ``"post_clear"`` actions to the 348 :data:`m2m_handler`. 349 350 335 351 class_prepared 336 352 -------------- 337 353 -
tests/modeltests/m2m_signals/tests.py
252 252 }) 253 253 self.assertEqual(self.m2m_changed_messages, expected_messages) 254 254 255 # direct assignment clearsthe set first, then adds255 # direct assignment removes objects from the set first, then adds 256 256 self.vw.default_parts = [self.wheelset,self.doors,self.engine] 257 257 expected_messages.append({ 258 258 'instance': self.vw, 259 'action': 'pre_ clear',259 'action': 'pre_remove', 260 260 'reverse': False, 261 261 'model': Part, 262 'objects': [p6], 262 263 }) 263 264 expected_messages.append({ 264 265 'instance': self.vw, 265 'action': 'post_ clear',266 'action': 'post_remove', 266 267 'reverse': False, 267 268 'model': Part, 269 'objects': [p6], 268 270 }) 269 271 expected_messages.append({ 270 272 'instance': self.vw, … … 282 284 }) 283 285 self.assertEqual(self.m2m_changed_messages, expected_messages) 284 286 285 # Check that signals still work when model inheritance is involved 286 c4 = SportsCar.objects.create(name='Bugatti', price='1000000') 287 c4b = Car.objects.get(name='Bugatti') 288 c4.default_parts = [self.doors] 287 # direct assignment can clear objects, if iterable is empty 288 self.vw.default_parts = [] 289 289 expected_messages.append({ 290 'instance': c4,290 'instance': self.vw, 291 291 'action': 'pre_clear', 292 292 'reverse': False, 293 293 'model': Part, 294 294 }) 295 295 expected_messages.append({ 296 'instance': c4,296 'instance': self.vw, 297 297 'action': 'post_clear', 298 298 'reverse': False, 299 299 'model': Part, 300 300 }) 301 self.assertEqual(self.m2m_changed_messages, expected_messages) 302 303 # Check that signals still work when model inheritance is involved 304 c4 = SportsCar.objects.create(name='Bugatti', price='1000000') 305 c4b = Car.objects.get(name='Bugatti') 306 c4.default_parts = [self.doors] 301 307 expected_messages.append({ 302 308 'instance': c4, 303 309 'action': 'pre_add', … … 344 350 self.alice.friends = [self.bob, self.chuck] 345 351 expected_messages.append({ 346 352 'instance': self.alice, 347 'action': 'pre_clear',348 'reverse': False,349 'model': Person,350 })351 expected_messages.append({352 'instance': self.alice,353 'action': 'post_clear',354 'reverse': False,355 'model': Person,356 })357 expected_messages.append({358 'instance': self.alice,359 353 'action': 'pre_add', 360 354 'reverse': False, 361 355 'model': Person, … … 373 367 self.alice.fans = [self.daisy] 374 368 expected_messages.append({ 375 369 'instance': self.alice, 376 'action': 'pre_clear',377 'reverse': False,378 'model': Person,379 })380 expected_messages.append({381 'instance': self.alice,382 'action': 'post_clear',383 'reverse': False,384 'model': Person,385 })386 expected_messages.append({387 'instance': self.alice,388 370 'action': 'pre_add', 389 371 'reverse': False, 390 372 'model': Person, … … 402 384 self.chuck.idols = [self.alice,self.bob] 403 385 expected_messages.append({ 404 386 'instance': self.chuck, 405 'action': 'pre_clear',406 'reverse': True,407 'model': Person,408 })409 expected_messages.append({410 'instance': self.chuck,411 'action': 'post_clear',412 'reverse': True,413 'model': Person,414 })415 expected_messages.append({416 'instance': self.chuck,417 387 'action': 'pre_add', 418 388 'reverse': True, 419 389 'model': Person, -
django/db/models/fields/related.py
574 574 # If the ManyToMany relation has an intermediary model, 575 575 # the add and remove methods do not exist. 576 576 if rel.through._meta.auto_created: 577 def add(self, *objs): 578 self._add_items(self.source_field_name, self.target_field_name, *objs) 577 def add(self, *objs, **kwargs): 578 check_duplicates = kwargs.get('check_duplicates', True) 579 self._add_items(self.source_field_name, self.target_field_name, 580 check_duplicates, *objs) 579 581 580 582 # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table 581 583 if self.symmetrical: 582 self._add_items(self.target_field_name, self.source_field_name, *objs) 584 self._add_items(self.target_field_name, self.source_field_name, 585 check_duplicates, *objs) 583 586 add.alters_data = True 584 587 585 588 def remove(self, *objs): … … 621 624 return obj, created 622 625 get_or_create.alters_data = True 623 626 624 def _add_items(self, source_field_name, target_field_name, *objs): 627 def _check_new_ids(self, objs): 628 from django.db.models import Model 629 new_ids = set() 630 for obj in objs: 631 if isinstance(obj, self.model): 632 if not router.allow_relation(obj, self.instance): 633 raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % 634 (obj, self.instance._state.db, obj._state.db)) 635 new_ids.add(obj.pk) 636 elif isinstance(obj, Model): 637 raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) 638 else: 639 new_ids.add(obj) 640 return new_ids 641 642 def _add_items(self, source_field_name, target_field_name, check_duplicates=True, *objs): 625 643 # source_field_name: the PK fieldname in join table for the source object 626 644 # target_field_name: the PK fieldname in join table for the target object 645 # check_duplicates: Checks to avoid adding duplicate objects if True. 627 646 # *objs - objects to add. Either object instances, or primary keys of object instances. 628 647 629 648 # If there aren't any objects, there is nothing to do. 630 649 from django.db.models import Model 631 650 if objs: 632 new_ids = set() 633 for obj in objs: 634 if isinstance(obj, self.model): 635 if not router.allow_relation(obj, self.instance): 636 raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % 637 (obj, self.instance._state.db, obj._state.db)) 638 new_ids.add(obj.pk) 639 elif isinstance(obj, Model): 640 raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) 641 else: 642 new_ids.add(obj) 651 new_ids = self._check_new_ids(objs) 643 652 db = router.db_for_write(self.through, instance=self.instance) 644 vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)645 vals = vals.filter(**{646 source_field_name: self._pk_val,647 '%s__in' % target_field_name: new_ids,648 })649 new_ids = new_ids - set(vals)650 653 654 if check_duplicates: 655 vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True) 656 vals = vals.filter(**{ 657 source_field_name: self._pk_val, 658 '%s__in' % target_field_name: new_ids, 659 }) 660 new_ids = new_ids - set(vals) 661 651 662 if self.reverse or source_field_name == self.source_field_name: 652 663 # Don't send the signal when we are inserting the 653 664 # duplicate data row for symmetrical reverse entries. … … 773 784 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)) 774 785 775 786 manager = self.__get__(instance) 776 manager.clear() 777 manager.add(*value) 787 if value: 788 new_ids = manager._check_new_ids(value) 789 old_ids = set(manager.values_list('pk', flat=True)) 778 790 791 manager.remove(*(old_ids - new_ids)) 792 manager.add(*(new_ids - old_ids), check_duplicates=False) 793 else: 794 manager.clear() 779 795 796 780 797 class ReverseManyRelatedObjectsDescriptor(object): 781 798 # This class provides the functionality that makes the related-object 782 799 # managers available as attributes on a model class, for fields that have … … 830 847 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)) 831 848 832 849 manager = self.__get__(instance) 833 manager.clear() 834 manager.add(*value) 850 if value: 851 new_ids = manager._check_new_ids(value) 852 old_ids = set(manager.values_list('pk', flat=True)) 835 853 854 manager.remove(*(old_ids - new_ids)) 855 manager.add(*(new_ids - old_ids), check_duplicates=False) 856 else: 857 manager.clear() 858 836 859 class ManyToOneRel(object): 837 860 def __init__(self, to, field_name, related_name=None, limit_choices_to=None, 838 861 parent_link=False, on_delete=None):