Ticket #6707: 6707_m2m_set.diff

File 6707_m2m_set.diff, 10.8 KB (added by sfllaw, 3 years ago)

Initial patch

  • docs/ref/signals.txt

     
    332332``using``       ``"default"`` (since the default router sends writes here)
    333333==============  ============================================================
    334334
     335We can also assign directly to the relation directly::
     336
     337    >>> p.toppings = [t]
     338
     339and the :data:`m2m_handler` will receive ``"pre_remove"`` and
     340``"post_remove"`` actions, if there were ``Topping`` objects to
     341remove. These are followed by ``"pre_add"`` and ``"post_add"`` actions.
     342
     343Assigning an empty iterable::
     344
     345   >>> p.toppings = []
     346
     347will send ``"pre_clear"`` and ``"post_clear"`` actions to the
     348:data:`m2m_handler`.
     349
     350
    335351class_prepared
    336352--------------
    337353
  • tests/modeltests/m2m_signals/tests.py

     
    252252        })
    253253        self.assertEqual(self.m2m_changed_messages, expected_messages)
    254254
    255         # direct assignment clears the set first, then adds
     255        # direct assignment removes objects from the set first, then adds
    256256        self.vw.default_parts = [self.wheelset,self.doors,self.engine]
    257257        expected_messages.append({
    258258            'instance': self.vw,
    259             'action': 'pre_clear',
     259            'action': 'pre_remove',
    260260            'reverse': False,
    261261            'model': Part,
     262            'objects': [p6],
    262263        })
    263264        expected_messages.append({
    264265            'instance': self.vw,
    265             'action': 'post_clear',
     266            'action': 'post_remove',
    266267            'reverse': False,
    267268            'model': Part,
     269            'objects': [p6],
    268270        })
    269271        expected_messages.append({
    270272            'instance': self.vw,
     
    282284        })
    283285        self.assertEqual(self.m2m_changed_messages, expected_messages)
    284286
    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 = []
    289289        expected_messages.append({
    290             'instance': c4,
     290            'instance': self.vw,
    291291            'action': 'pre_clear',
    292292            'reverse': False,
    293293            'model': Part,
    294294        })
    295295        expected_messages.append({
    296             'instance': c4,
     296            'instance': self.vw,
    297297            'action': 'post_clear',
    298298            'reverse': False,
    299299            'model': Part,
    300300        })
     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]
    301307        expected_messages.append({
    302308            'instance': c4,
    303309            'action': 'pre_add',
     
    344350        self.alice.friends = [self.bob, self.chuck]
    345351        expected_messages.append({
    346352            '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,
    359353            'action': 'pre_add',
    360354            'reverse': False,
    361355            'model': Person,
     
    373367        self.alice.fans = [self.daisy]
    374368        expected_messages.append({
    375369            '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,
    388370            'action': 'pre_add',
    389371            'reverse': False,
    390372            'model': Person,
     
    402384        self.chuck.idols = [self.alice,self.bob]
    403385        expected_messages.append({
    404386            '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,
    417387            'action': 'pre_add',
    418388            'reverse': True,
    419389            'model': Person,
  • django/db/models/fields/related.py

     
    574574        # If the ManyToMany relation has an intermediary model,
    575575        # the add and remove methods do not exist.
    576576        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)
    579581
    580582                # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
    581583                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)
    583586            add.alters_data = True
    584587
    585588            def remove(self, *objs):
     
    621624            return obj, created
    622625        get_or_create.alters_data = True
    623626
    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):
    625643            # source_field_name: the PK fieldname in join table for the source object
    626644            # target_field_name: the PK fieldname in join table for the target object
     645            # check_duplicates: Checks to avoid adding duplicate objects if True.
    627646            # *objs - objects to add. Either object instances, or primary keys of object instances.
    628647
    629648            # If there aren't any objects, there is nothing to do.
    630649            from django.db.models import Model
    631650            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)
    643652                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)
    650653
     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
    651662                if self.reverse or source_field_name == self.source_field_name:
    652663                    # Don't send the signal when we are inserting the
    653664                    # duplicate data row for symmetrical reverse entries.
     
    773784            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))
    774785
    775786        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))
    778790
     791            manager.remove(*(old_ids - new_ids))
     792            manager.add(*(new_ids - old_ids), check_duplicates=False)
     793        else:
     794            manager.clear()
    779795
     796
    780797class ReverseManyRelatedObjectsDescriptor(object):
    781798    # This class provides the functionality that makes the related-object
    782799    # managers available as attributes on a model class, for fields that have
     
    830847            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))
    831848
    832849        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))
    835853
     854            manager.remove(*(old_ids - new_ids))
     855            manager.add(*(new_ids - old_ids), check_duplicates=False)
     856        else:
     857            manager.clear()
     858
    836859class ManyToOneRel(object):
    837860    def __init__(self, to, field_name, related_name=None, limit_choices_to=None,
    838861        parent_link=False, on_delete=None):
Back to Top