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 4728233a6a..40ad8b260f 100644 --- a/django/db/models/fields/related_descriptors.py +++ b/django/db/models/fields/related_descriptors.py @@ -1262,15 +1262,16 @@ 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, raw=False): + 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, + raw=raw, ) # If this is a symmetrical m2m relation to self, add the mirror # entry in the m2m table. @@ -1280,8 +1281,15 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): self.source_field_name, *objs, through_defaults=through_defaults, + using=db, + raw=raw, ) + 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 +1299,16 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): aadd.alters_data = True + 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, raw=raw + ) + 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 +1317,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, 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( sender=self.through, @@ -1313,8 +1328,8 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=None, using=db, + raw=raw, ) - self._remove_prefetched_objects() filters = self._build_remove_filters(super().get_queryset().using(db)) self.through._default_manager.using(db).filter(filters).delete() @@ -1326,8 +1341,14 @@ def create_forward_many_to_many_manager(superclass, rel, reverse): model=self.model, pk_set=None, using=db, + raw=raw, ) + 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): @@ -1335,16 +1356,19 @@ 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, 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) 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, raw=raw) + self._add_base( + *objs, through_defaults=through_defaults, using=db, raw=raw + ) else: old_ids = set( self.using(db).values_list( @@ -1364,8 +1388,13 @@ 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, raw=raw) + self._add_base( + *new_objs, through_defaults=through_defaults, using=db, raw=raw + ) + + def set(self, objs, *, clear=False, through_defaults=None): + self.set_base(objs, clear=clear, through_defaults=through_defaults) set.alters_data = True @@ -1516,7 +1545,13 @@ 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, + 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 @@ -1527,7 +1562,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 ) @@ -1559,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( @@ -1584,9 +1620,12 @@ 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): + def _remove_items( + 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 # target object *objs - objects to remove. Either object instances, @@ -1603,7 +1642,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( @@ -1614,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(): @@ -1633,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 425419529b..8c498874f2 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -228,7 +228,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]