From 395454b2e8f86f735a67dbbccd3575ce7b8ff90b Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 7 Nov 2025 09:39:42 +0100 Subject: [PATCH 1/4] Refs #27380 -- Made ManyRelatedManager.add()/clear()/remove() implementation more consistent. This also adds _add_base(), _clear_base(), and _remove_base() internal hooks. --- .../db/models/fields/related_descriptors.py | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 4728233a6a..6056af3660 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -1262,15 +1262,15 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): else: return super().count() - def add(self, *objs, through_defaults=None): - self._remove_prefetched_objects() - db = router.db_for_write(self.through, instance=self.instance) + def _add_base(self, *objs, through_defaults=None, using=None): + db = using or router.db_for_write(self.through, instance=self.instance) with transaction.atomic(using=db, savepoint=False): self._add_items( self.source_field_name, self.target_field_name, *objs, through_defaults=through_defaults, + using=db, ) # If this is a symmetrical m2m relation to self, add the mirror # entry in the m2m table. @@ -1280,8 +1280,14 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): self.source_field_name, *objs, through_defaults=through_defaults, + using=db, ) + def add(self, *objs, through_defaults=None): + self._remove_prefetched_objects() + db = router.db_for_write(self.through, instance=self.instance) + self._add_base(*objs, through_defaults=through_defaults, using=db) + add.alters_data = True async def aadd(self, *objs, through_defaults=None): @@ -1291,9 +1297,16 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): aadd.alters_data = True + def _remove_base(self, *objs, using=None): + db = using or router.db_for_write(self.through, instance=self.instance) + self._remove_items( + self.source_field_name, self.target_field_name, *objs, using=db + ) + def remove(self, *objs): self._remove_prefetched_objects() - self._remove_items(self.source_field_name, self.target_field_name, *objs) + db = router.db_for_write(self.through, instance=self.instance) + self._remove_base(*objs, using=db) remove.alters_data = True @@ -1302,8 +1315,8 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): aremove.alters_data = True - def clear(self): - db = router.db_for_write(self.through, instance=self.instance) + def _clear_base(self, using=None): + db = using or router.db_for_write(self.through, instance=self.instance) with transaction.atomic(using=db, savepoint=False): signals.m2m_changed.send( sender=self.through, @@ -1314,7 +1327,6 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): pk_set=None, using=db, ) - self._remove_prefetched_objects() filters = self._build_remove_filters(super().get_queryset().using(db)) self.through._default_manager.using(db).filter(filters).delete() @@ -1328,6 +1340,11 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): using=db, ) + def clear(self): + self._remove_prefetched_objects() + db = router.db_for_write(self.through, instance=self.instance) + self._clear_base(using=db) + clear.alters_data = True async def aclear(self): @@ -1516,7 +1533,12 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): ) def _add_items( - self, source_field_name, target_field_name, *objs, through_defaults=None + self, + source_field_name, + target_field_name, + *objs, + through_defaults=None, + using=None, ): # source_field_name: the PK fieldname in join table for the source # object target_field_name: the PK fieldname in join table for the @@ -1527,7 +1549,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): through_defaults = dict(resolve_callables(through_defaults or {})) target_ids = self._get_target_ids(target_field_name, objs) - db = router.db_for_write(self.through, instance=self.instance) + db = using or router.db_for_write(self.through, instance=self.instance) can_ignore_conflicts, must_send_signals, can_fast_add = self._get_add_plan( db, source_field_name ) @@ -1586,7 +1608,9 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): using=db, ) - def _remove_items(self, source_field_name, target_field_name, *objs): + def _remove_items( + self, source_field_name, target_field_name, *objs, using=None + ): # source_field_name: the PK colname in join table for the source # object target_field_name: the PK colname in join table for the # target object *objs - objects to remove. Either object instances, @@ -1603,7 +1627,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): else: old_ids.add(obj) - db = router.db_for_write(self.through, instance=self.instance) + db = using or router.db_for_write(self.through, instance=self.instance) with transaction.atomic(using=db, savepoint=False): # Send a signal to the other end if need be. signals.m2m_changed.send( From ba3dbf1c55cdb34ab33c271a661b2e9d9dd839d6 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 7 Nov 2025 09:57:31 +0100 Subject: [PATCH 2/4] Refs #27380 -- Made ManyRelatedManager.set() use *_base() hooks. --- django/db/models/fields/related_descriptors.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 6056af3660..866fc58fc2 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -1359,9 +1359,10 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): db = router.db_for_write(self.through, instance=self.instance) with transaction.atomic(using=db, savepoint=False): + self._remove_prefetched_objects() if clear: - self.clear() - self.add(*objs, through_defaults=through_defaults) + self._clear_base(using=db) + self._add_base(*objs, through_defaults=through_defaults, using=db) else: old_ids = set( self.using(db).values_list( @@ -1381,8 +1382,10 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): else: new_objs.append(obj) - self.remove(*old_ids) - self.add(*new_objs, through_defaults=through_defaults) + self._remove_base(*old_ids, using=db) + self._add_base( + *new_objs, through_defaults=through_defaults, using=db + ) set.alters_data = True From 5e6ba7368c5d694d77a0f7d634334dbf9ce69a24 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 7 Nov 2025 10:07:21 +0100 Subject: [PATCH 3/4] Refs #27380 -- Added ManyRelatedManager.set_base() hook. --- django/db/models/fields/related_descriptors.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 866fc58fc2..622c49837f 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -1352,7 +1352,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): aclear.alters_data = True - def set(self, objs, *, clear=False, through_defaults=None): + def set_base(self, objs, *, clear=False, through_defaults=None): # Force evaluation of `objs` in case it's a queryset whose value # could be affected by `manager.clear()`. Refs #19816. objs = tuple(objs) @@ -1387,6 +1387,9 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): *new_objs, through_defaults=through_defaults, using=db ) + def set(self, objs, *, clear=False, through_defaults=None): + self.set_base(objs, clear=clear, through_defaults=through_defaults) + set.alters_data = True async def aset(self, objs, *, clear=False, through_defaults=None): From 47f8206609bd00a9b85e19beffe77ae133838a1f Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 7 Nov 2025 10:17:22 +0100 Subject: [PATCH 4/4] Fixed #27380 -- Added "raw" argument to m2m_changed signals. --- django/core/serializers/base.py | 4 +- .../db/models/fields/related_descriptors.py | 31 +++-- docs/ref/signals.txt | 9 ++ docs/releases/6.1.txt | 4 +- docs/topics/db/fixtures.txt | 9 +- tests/m2m_signals/tests.py | 123 ++++++++++++++++++ tests/serializers/test_data.py | 2 +- 7 files changed, 166 insertions(+), 16 deletions(-) diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index efc55981eb..3b05a9569c 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -262,11 +262,11 @@ class DeserializedObject: def save(self, save_m2m=True, using=None, **kwargs): # Call save on the Model baseclass directly. This bypasses any # model-defined save. The save is also forced to be raw. - # raw=True is passed to any pre/post_save signals. + # raw=True is passed to any pre/post_save and m2m_changed signals. models.Model.save_base(self.object, using=using, raw=True, **kwargs) if self.m2m_data and save_m2m: for accessor_name, object_list in self.m2m_data.items(): - getattr(self.object, accessor_name).set(object_list) + getattr(self.object, accessor_name).set_base(object_list, raw=True) # prevent a second (possibly accidental) call to save() from saving # the m2m data twice. diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py index 622c49837f..40ad8b260f 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -1262,7 +1262,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): else: return super().count() - def _add_base(self, *objs, through_defaults=None, using=None): + def _add_base(self, *objs, through_defaults=None, using=None, raw=False): db = using or router.db_for_write(self.through, instance=self.instance) with transaction.atomic(using=db, savepoint=False): self._add_items( @@ -1271,6 +1271,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): *objs, through_defaults=through_defaults, using=db, + raw=raw, ) # If this is a symmetrical m2m relation to self, add the mirror # entry in the m2m table. @@ -1281,6 +1282,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): *objs, through_defaults=through_defaults, using=db, + raw=raw, ) def add(self, *objs, through_defaults=None): @@ -1297,10 +1299,10 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): aadd.alters_data = True - def _remove_base(self, *objs, using=None): + def _remove_base(self, *objs, using=None, raw=False): db = using or router.db_for_write(self.through, instance=self.instance) self._remove_items( - self.source_field_name, self.target_field_name, *objs, using=db + self.source_field_name, self.target_field_name, *objs, using=db, raw=raw ) def remove(self, *objs): @@ -1315,7 +1317,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): aremove.alters_data = True - def _clear_base(self, using=None): + def _clear_base(self, using=None, raw=False): db = using or router.db_for_write(self.through, instance=self.instance) with transaction.atomic(using=db, savepoint=False): signals.m2m_changed.send( @@ -1326,6 +1328,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=None, using=db, + raw=raw, ) filters = self._build_remove_filters(super().get_queryset().using(db)) self.through._default_manager.using(db).filter(filters).delete() @@ -1338,6 +1341,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=None, using=db, + raw=raw, ) def clear(self): @@ -1352,7 +1356,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): aclear.alters_data = True - def set_base(self, objs, *, clear=False, through_defaults=None): + def set_base(self, objs, *, clear=False, through_defaults=None, raw=False): # Force evaluation of `objs` in case it's a queryset whose value # could be affected by `manager.clear()`. Refs #19816. objs = tuple(objs) @@ -1361,8 +1365,10 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): with transaction.atomic(using=db, savepoint=False): self._remove_prefetched_objects() if clear: - self._clear_base(using=db) - self._add_base(*objs, through_defaults=through_defaults, using=db) + self._clear_base(using=db, raw=raw) + self._add_base( + *objs, through_defaults=through_defaults, using=db, raw=raw + ) else: old_ids = set( self.using(db).values_list( @@ -1382,9 +1388,9 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): else: new_objs.append(obj) - self._remove_base(*old_ids, using=db) + self._remove_base(*old_ids, using=db, raw=raw) self._add_base( - *new_objs, through_defaults=through_defaults, using=db + *new_objs, through_defaults=through_defaults, using=db, raw=raw ) def set(self, objs, *, clear=False, through_defaults=None): @@ -1545,6 +1551,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): *objs, through_defaults=None, using=None, + raw=False, ): # source_field_name: the PK fieldname in join table for the source # object target_field_name: the PK fieldname in join table for the @@ -1587,6 +1594,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=missing_target_ids, using=db, + raw=raw, ) # Add the ones that aren't there already. self.through._default_manager.using(db).bulk_create( @@ -1612,10 +1620,11 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=missing_target_ids, using=db, + raw=raw, ) def _remove_items( - self, source_field_name, target_field_name, *objs, using=None + self, source_field_name, target_field_name, *objs, using=None, raw=False ): # source_field_name: the PK colname in join table for the source # object target_field_name: the PK colname in join table for the @@ -1644,6 +1653,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=old_ids, using=db, + raw=raw, ) target_model_qs = super().get_queryset() if target_model_qs._has_filters(): @@ -1663,6 +1673,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=old_ids, using=db, + raw=raw, ) return ManyRelatedManager diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 44958dcef3..e251c2f062 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -303,6 +303,15 @@ Arguments sent with this signal: ``using`` The database alias being used. +``raw`` + + .. versionadded:: 6.1 + + A boolean; ``True`` if the model is saved exactly as presented + (i.e. when loading a :ref:`fixture `). One should not + query/modify other records in the database as the database might not be in + a consistent state yet. + For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled like this:: diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 036fee53cf..6b4142514d 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -225,7 +225,9 @@ Logging Management Commands ~~~~~~~~~~~~~~~~~~~ -* ... +* The :djadmin:`loaddata` command now calls + :data:`~django.db.models.signals.m2m_changed` signals with ``raw=True`` when + loading fixtures. Migrations ~~~~~~~~~~ diff --git a/docs/topics/db/fixtures.txt b/docs/topics/db/fixtures.txt index 6066d34f8e..9e91e0d845 100644 --- a/docs/topics/db/fixtures.txt +++ b/docs/topics/db/fixtures.txt @@ -118,8 +118,9 @@ How fixtures are saved to the database When fixture files are processed, the data is saved to the database as is. Model defined :meth:`~django.db.models.Model.save` methods are not called, and -any :data:`~django.db.models.signals.pre_save` or -:data:`~django.db.models.signals.post_save` signals will be called with +any :data:`~django.db.models.signals.pre_save`, +:data:`~django.db.models.signals.post_save`, or +:data:`~django.db.models.signals.m2m_changed` signals will be called with ``raw=True`` since the instance only contains attributes that are local to the model. You may, for example, want to disable handlers that access related fields that aren't present during fixture loading and would otherwise @@ -163,6 +164,10 @@ You could also write a decorator to encapsulate this logic:: Just be aware that this logic will disable the signals whenever fixtures are deserialized, not just during :djadmin:`loaddata`. +.. versionchanged:: 6.1 + + The ``raw`` argument was added to ``m2m_changed`` signals. + Compressed fixtures =================== diff --git a/tests/m2m_signals/tests.py b/tests/m2m_signals/tests.py index 488c0a6fbe..980771d54b 100644 --- a/tests/m2m_signals/tests.py +++ b/tests/m2m_signals/tests.py @@ -35,6 +35,7 @@ class ManyToManySignalsTest(TestCase): "action": kwargs["action"], "reverse": kwargs["reverse"], "model": kwargs["model"], + "raw": kwargs["raw"], } if kwargs["pk_set"]: message["objects"] = list( @@ -114,6 +115,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors, self.engine, self.wheelset], } ) @@ -123,6 +125,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors, self.engine, self.wheelset], } ) @@ -136,6 +139,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": True, "model": Car, + "raw": False, "objects": [self.bmw, self.toyota], } ) @@ -145,6 +149,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": True, "model": Car, + "raw": False, "objects": [self.bmw, self.toyota], } ) @@ -163,6 +168,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_remove", "reverse": False, "model": Part, + "raw": False, "objects": [self.airbag, self.engine], }, { @@ -170,6 +176,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_remove", "reverse": False, "model": Part, + "raw": False, "objects": [self.airbag, self.engine], }, ], @@ -188,6 +195,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.airbag, self.sunroof], } ) @@ -197,6 +205,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.airbag, self.sunroof], } ) @@ -210,6 +219,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": True, "model": Car, + "raw": False, "objects": [self.bmw, self.toyota], } ) @@ -219,6 +229,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": True, "model": Car, + "raw": False, "objects": [self.bmw, self.toyota], } ) @@ -237,6 +248,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_remove", "reverse": True, "model": Car, + "raw": False, "objects": [self.vw], }, { @@ -244,6 +256,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_remove", "reverse": True, "model": Car, + "raw": False, "objects": [self.vw], }, ], @@ -261,12 +274,14 @@ class ManyToManySignalsTest(TestCase): "action": "pre_clear", "reverse": False, "model": Part, + "raw": False, }, { "instance": self.vw, "action": "post_clear", "reverse": False, "model": Part, + "raw": False, }, ], ) @@ -283,12 +298,14 @@ class ManyToManySignalsTest(TestCase): "action": "pre_clear", "reverse": True, "model": Car, + "raw": False, }, { "instance": self.doors, "action": "post_clear", "reverse": True, "model": Car, + "raw": False, }, ], ) @@ -306,12 +323,14 @@ class ManyToManySignalsTest(TestCase): "action": "pre_clear", "reverse": True, "model": Car, + "raw": False, }, { "instance": self.airbag, "action": "post_clear", "reverse": True, "model": Car, + "raw": False, }, ], ) @@ -330,6 +349,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Part, + "raw": False, "objects": [p6], } ) @@ -339,6 +359,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Part, + "raw": False, "objects": [p6], } ) @@ -352,6 +373,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_remove", "reverse": False, "model": Part, + "raw": False, "objects": [p6], } ) @@ -361,6 +383,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_remove", "reverse": False, "model": Part, + "raw": False, "objects": [p6], } ) @@ -370,6 +393,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors, self.engine, self.wheelset], } ) @@ -379,6 +403,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors, self.engine, self.wheelset], } ) @@ -397,6 +422,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_clear", "reverse": False, "model": Part, + "raw": False, } ) expected_messages.append( @@ -405,6 +431,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_clear", "reverse": False, "model": Part, + "raw": False, } ) expected_messages.append( @@ -413,6 +440,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors, self.engine, self.wheelset], } ) @@ -422,6 +450,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors, self.engine, self.wheelset], } ) @@ -435,6 +464,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_remove", "reverse": False, "model": Part, + "raw": False, "objects": [self.engine], } ) @@ -444,6 +474,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_remove", "reverse": False, "model": Part, + "raw": False, "objects": [self.engine], } ) @@ -464,6 +495,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors], } ) @@ -473,6 +505,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Part, + "raw": False, "objects": [self.doors], } ) @@ -485,6 +518,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": True, "model": Car, + "raw": False, "objects": [c4b], } ) @@ -494,6 +528,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": True, "model": Car, + "raw": False, "objects": [c4b], } ) @@ -519,6 +554,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Person, + "raw": False, "objects": [self.bob, self.chuck], }, { @@ -526,6 +562,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Person, + "raw": False, "objects": [self.bob, self.chuck], }, ], @@ -542,6 +579,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": False, "model": Person, + "raw": False, "objects": [self.daisy], }, { @@ -549,6 +587,7 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": False, "model": Person, + "raw": False, "objects": [self.daisy], }, ], @@ -565,6 +604,7 @@ class ManyToManySignalsTest(TestCase): "action": "pre_add", "reverse": True, "model": Person, + "raw": False, "objects": [self.alice, self.bob], }, { @@ -572,6 +612,89 @@ class ManyToManySignalsTest(TestCase): "action": "post_add", "reverse": True, "model": Person, + "raw": False, + "objects": [self.alice, self.bob], + }, + ], + ) + + def test_m2m_relations_set_base_raw(self): + self.chuck.idols.add(self.daisy) + self._initialize_signal_person() + self.chuck.idols.set_base([self.alice, self.bob], raw=True) + self.assertEqual( + self.m2m_changed_messages, + [ + { + "instance": self.chuck, + "action": "pre_remove", + "reverse": True, + "model": Person, + "raw": True, + "objects": [self.daisy], + }, + { + "instance": self.chuck, + "action": "post_remove", + "reverse": True, + "model": Person, + "raw": True, + "objects": [self.daisy], + }, + { + "instance": self.chuck, + "action": "pre_add", + "reverse": True, + "model": Person, + "raw": True, + "objects": [self.alice, self.bob], + }, + { + "instance": self.chuck, + "action": "post_add", + "reverse": True, + "model": Person, + "raw": True, + "objects": [self.alice, self.bob], + }, + ], + ) + + def test_m2m_relations_set_base_raw_clear(self): + self.chuck.idols.set([self.daisy, self.bob]) + self._initialize_signal_person() + self.chuck.idols.set_base([self.alice, self.bob], clear=True, raw=True) + self.assertEqual( + self.m2m_changed_messages, + [ + { + "instance": self.chuck, + "action": "pre_clear", + "reverse": True, + "model": Person, + "raw": True, + }, + { + "instance": self.chuck, + "action": "post_clear", + "reverse": True, + "model": Person, + "raw": True, + }, + { + "instance": self.chuck, + "action": "pre_add", + "reverse": True, + "model": Person, + "raw": True, + "objects": [self.alice, self.bob], + }, + { + "instance": self.chuck, + "action": "post_add", + "reverse": True, + "model": Person, + "raw": True, "objects": [self.alice, self.bob], }, ], diff --git a/tests/serializers/test_data.py b/tests/serializers/test_data.py index c626f2550a..121404a0ef 100644 --- a/tests/serializers/test_data.py +++ b/tests/serializers/test_data.py @@ -112,7 +112,7 @@ def fk_create(pk, klass, data): def m2m_create(pk, klass, data): instance = klass(id=pk) models.Model.save_base(instance, raw=True) - instance.data.set(data) + instance.data.set_base(data, raw=True) return [instance]