This commit is contained in:
Mariusz Felisiak 2025-11-17 13:45:10 +01:00 committed by GitHub
commit 737bddeb6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 202 additions and 22 deletions

View file

@ -262,11 +262,11 @@ class DeserializedObject:
def save(self, save_m2m=True, using=None, **kwargs): def save(self, save_m2m=True, using=None, **kwargs):
# Call save on the Model baseclass directly. This bypasses any # Call save on the Model baseclass directly. This bypasses any
# model-defined save. The save is also forced to be raw. # 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) models.Model.save_base(self.object, using=using, raw=True, **kwargs)
if self.m2m_data and save_m2m: if self.m2m_data and save_m2m:
for accessor_name, object_list in self.m2m_data.items(): 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 # prevent a second (possibly accidental) call to save() from saving
# the m2m data twice. # the m2m data twice.

View file

@ -1262,15 +1262,16 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
else: else:
return super().count() return super().count()
def add(self, *objs, through_defaults=None): def _add_base(self, *objs, through_defaults=None, using=None, raw=False):
self._remove_prefetched_objects() db = using or router.db_for_write(self.through, instance=self.instance)
db = router.db_for_write(self.through, instance=self.instance)
with transaction.atomic(using=db, savepoint=False): with transaction.atomic(using=db, savepoint=False):
self._add_items( self._add_items(
self.source_field_name, self.source_field_name,
self.target_field_name, self.target_field_name,
*objs, *objs,
through_defaults=through_defaults, through_defaults=through_defaults,
using=db,
raw=raw,
) )
# If this is a symmetrical m2m relation to self, add the mirror # If this is a symmetrical m2m relation to self, add the mirror
# entry in the m2m table. # entry in the m2m table.
@ -1280,8 +1281,15 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
self.source_field_name, self.source_field_name,
*objs, *objs,
through_defaults=through_defaults, 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 add.alters_data = True
async def aadd(self, *objs, through_defaults=None): 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 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): def remove(self, *objs):
self._remove_prefetched_objects() 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 remove.alters_data = True
@ -1302,8 +1317,8 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
aremove.alters_data = True aremove.alters_data = True
def clear(self): def _clear_base(self, using=None, raw=False):
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): with transaction.atomic(using=db, savepoint=False):
signals.m2m_changed.send( signals.m2m_changed.send(
sender=self.through, sender=self.through,
@ -1313,8 +1328,8 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
model=self.model, model=self.model,
pk_set=None, pk_set=None,
using=db, using=db,
raw=raw,
) )
self._remove_prefetched_objects()
filters = self._build_remove_filters(super().get_queryset().using(db)) filters = self._build_remove_filters(super().get_queryset().using(db))
self.through._default_manager.using(db).filter(filters).delete() 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, model=self.model,
pk_set=None, pk_set=None,
using=db, 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 clear.alters_data = True
async def aclear(self): async def aclear(self):
@ -1335,16 +1356,19 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
aclear.alters_data = True 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 # Force evaluation of `objs` in case it's a queryset whose value
# could be affected by `manager.clear()`. Refs #19816. # could be affected by `manager.clear()`. Refs #19816.
objs = tuple(objs) objs = tuple(objs)
db = router.db_for_write(self.through, instance=self.instance) db = router.db_for_write(self.through, instance=self.instance)
with transaction.atomic(using=db, savepoint=False): with transaction.atomic(using=db, savepoint=False):
self._remove_prefetched_objects()
if clear: if clear:
self.clear() self._clear_base(using=db, raw=raw)
self.add(*objs, through_defaults=through_defaults) self._add_base(
*objs, through_defaults=through_defaults, using=db, raw=raw
)
else: else:
old_ids = set( old_ids = set(
self.using(db).values_list( self.using(db).values_list(
@ -1364,8 +1388,13 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
else: else:
new_objs.append(obj) new_objs.append(obj)
self.remove(*old_ids) self._remove_base(*old_ids, using=db, raw=raw)
self.add(*new_objs, through_defaults=through_defaults) 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 set.alters_data = True
@ -1516,7 +1545,13 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
) )
def _add_items( 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 # source_field_name: the PK fieldname in join table for the source
# object target_field_name: the PK fieldname in join table for the # 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 {})) through_defaults = dict(resolve_callables(through_defaults or {}))
target_ids = self._get_target_ids(target_field_name, objs) 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( can_ignore_conflicts, must_send_signals, can_fast_add = self._get_add_plan(
db, source_field_name db, source_field_name
) )
@ -1559,6 +1594,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
model=self.model, model=self.model,
pk_set=missing_target_ids, pk_set=missing_target_ids,
using=db, using=db,
raw=raw,
) )
# Add the ones that aren't there already. # Add the ones that aren't there already.
self.through._default_manager.using(db).bulk_create( 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, model=self.model,
pk_set=missing_target_ids, pk_set=missing_target_ids,
using=db, 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 # source_field_name: the PK colname in join table for the source
# object target_field_name: the PK colname in join table for the # object target_field_name: the PK colname in join table for the
# target object *objs - objects to remove. Either object instances, # target object *objs - objects to remove. Either object instances,
@ -1603,7 +1642,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
else: else:
old_ids.add(obj) 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): with transaction.atomic(using=db, savepoint=False):
# Send a signal to the other end if need be. # Send a signal to the other end if need be.
signals.m2m_changed.send( signals.m2m_changed.send(
@ -1614,6 +1653,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
model=self.model, model=self.model,
pk_set=old_ids, pk_set=old_ids,
using=db, using=db,
raw=raw,
) )
target_model_qs = super().get_queryset() target_model_qs = super().get_queryset()
if target_model_qs._has_filters(): if target_model_qs._has_filters():
@ -1633,6 +1673,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
model=self.model, model=self.model,
pk_set=old_ids, pk_set=old_ids,
using=db, using=db,
raw=raw,
) )
return ManyRelatedManager return ManyRelatedManager

View file

@ -303,6 +303,15 @@ Arguments sent with this signal:
``using`` ``using``
The database alias being used. 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 <fixtures-explanation>`). 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 For example, if a ``Pizza`` can have multiple ``Topping`` objects, modeled
like this:: like this::

View file

@ -228,7 +228,9 @@ Logging
Management Commands Management Commands
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
* ... * The :djadmin:`loaddata` command now calls
:data:`~django.db.models.signals.m2m_changed` signals with ``raw=True`` when
loading fixtures.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~

View file

@ -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. 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 Model defined :meth:`~django.db.models.Model.save` methods are not called, and
any :data:`~django.db.models.signals.pre_save` or any :data:`~django.db.models.signals.pre_save`,
:data:`~django.db.models.signals.post_save` signals will be called with :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 ``raw=True`` since the instance only contains attributes that are local to the
model. You may, for example, want to disable handlers that access model. You may, for example, want to disable handlers that access
related fields that aren't present during fixture loading and would otherwise 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 Just be aware that this logic will disable the signals whenever fixtures are
deserialized, not just during :djadmin:`loaddata`. deserialized, not just during :djadmin:`loaddata`.
.. versionchanged:: 6.1
The ``raw`` argument was added to ``m2m_changed`` signals.
Compressed fixtures Compressed fixtures
=================== ===================

View file

@ -35,6 +35,7 @@ class ManyToManySignalsTest(TestCase):
"action": kwargs["action"], "action": kwargs["action"],
"reverse": kwargs["reverse"], "reverse": kwargs["reverse"],
"model": kwargs["model"], "model": kwargs["model"],
"raw": kwargs["raw"],
} }
if kwargs["pk_set"]: if kwargs["pk_set"]:
message["objects"] = list( message["objects"] = list(
@ -114,6 +115,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors, self.engine, self.wheelset], "objects": [self.doors, self.engine, self.wheelset],
} }
) )
@ -123,6 +125,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors, self.engine, self.wheelset], "objects": [self.doors, self.engine, self.wheelset],
} }
) )
@ -136,6 +139,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [self.bmw, self.toyota], "objects": [self.bmw, self.toyota],
} }
) )
@ -145,6 +149,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [self.bmw, self.toyota], "objects": [self.bmw, self.toyota],
} }
) )
@ -163,6 +168,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_remove", "action": "pre_remove",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.airbag, self.engine], "objects": [self.airbag, self.engine],
}, },
{ {
@ -170,6 +176,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_remove", "action": "post_remove",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.airbag, self.engine], "objects": [self.airbag, self.engine],
}, },
], ],
@ -188,6 +195,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.airbag, self.sunroof], "objects": [self.airbag, self.sunroof],
} }
) )
@ -197,6 +205,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.airbag, self.sunroof], "objects": [self.airbag, self.sunroof],
} }
) )
@ -210,6 +219,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [self.bmw, self.toyota], "objects": [self.bmw, self.toyota],
} }
) )
@ -219,6 +229,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [self.bmw, self.toyota], "objects": [self.bmw, self.toyota],
} }
) )
@ -237,6 +248,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_remove", "action": "pre_remove",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [self.vw], "objects": [self.vw],
}, },
{ {
@ -244,6 +256,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_remove", "action": "post_remove",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [self.vw], "objects": [self.vw],
}, },
], ],
@ -261,12 +274,14 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_clear", "action": "pre_clear",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
}, },
{ {
"instance": self.vw, "instance": self.vw,
"action": "post_clear", "action": "post_clear",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
}, },
], ],
) )
@ -283,12 +298,14 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_clear", "action": "pre_clear",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
}, },
{ {
"instance": self.doors, "instance": self.doors,
"action": "post_clear", "action": "post_clear",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
}, },
], ],
) )
@ -306,12 +323,14 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_clear", "action": "pre_clear",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
}, },
{ {
"instance": self.airbag, "instance": self.airbag,
"action": "post_clear", "action": "post_clear",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
}, },
], ],
) )
@ -330,6 +349,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [p6], "objects": [p6],
} }
) )
@ -339,6 +359,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [p6], "objects": [p6],
} }
) )
@ -352,6 +373,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_remove", "action": "pre_remove",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [p6], "objects": [p6],
} }
) )
@ -361,6 +383,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_remove", "action": "post_remove",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [p6], "objects": [p6],
} }
) )
@ -370,6 +393,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors, self.engine, self.wheelset], "objects": [self.doors, self.engine, self.wheelset],
} }
) )
@ -379,6 +403,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors, self.engine, self.wheelset], "objects": [self.doors, self.engine, self.wheelset],
} }
) )
@ -397,6 +422,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_clear", "action": "pre_clear",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
} }
) )
expected_messages.append( expected_messages.append(
@ -405,6 +431,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_clear", "action": "post_clear",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
} }
) )
expected_messages.append( expected_messages.append(
@ -413,6 +440,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors, self.engine, self.wheelset], "objects": [self.doors, self.engine, self.wheelset],
} }
) )
@ -422,6 +450,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors, self.engine, self.wheelset], "objects": [self.doors, self.engine, self.wheelset],
} }
) )
@ -435,6 +464,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_remove", "action": "pre_remove",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.engine], "objects": [self.engine],
} }
) )
@ -444,6 +474,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_remove", "action": "post_remove",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.engine], "objects": [self.engine],
} }
) )
@ -464,6 +495,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors], "objects": [self.doors],
} }
) )
@ -473,6 +505,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Part, "model": Part,
"raw": False,
"objects": [self.doors], "objects": [self.doors],
} }
) )
@ -485,6 +518,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [c4b], "objects": [c4b],
} }
) )
@ -494,6 +528,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": True, "reverse": True,
"model": Car, "model": Car,
"raw": False,
"objects": [c4b], "objects": [c4b],
} }
) )
@ -519,6 +554,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Person, "model": Person,
"raw": False,
"objects": [self.bob, self.chuck], "objects": [self.bob, self.chuck],
}, },
{ {
@ -526,6 +562,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Person, "model": Person,
"raw": False,
"objects": [self.bob, self.chuck], "objects": [self.bob, self.chuck],
}, },
], ],
@ -542,6 +579,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": False, "reverse": False,
"model": Person, "model": Person,
"raw": False,
"objects": [self.daisy], "objects": [self.daisy],
}, },
{ {
@ -549,6 +587,7 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": False, "reverse": False,
"model": Person, "model": Person,
"raw": False,
"objects": [self.daisy], "objects": [self.daisy],
}, },
], ],
@ -565,6 +604,7 @@ class ManyToManySignalsTest(TestCase):
"action": "pre_add", "action": "pre_add",
"reverse": True, "reverse": True,
"model": Person, "model": Person,
"raw": False,
"objects": [self.alice, self.bob], "objects": [self.alice, self.bob],
}, },
{ {
@ -572,6 +612,89 @@ class ManyToManySignalsTest(TestCase):
"action": "post_add", "action": "post_add",
"reverse": True, "reverse": True,
"model": Person, "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], "objects": [self.alice, self.bob],
}, },
], ],

View file

@ -112,7 +112,7 @@ def fk_create(pk, klass, data):
def m2m_create(pk, klass, data): def m2m_create(pk, klass, data):
instance = klass(id=pk) instance = klass(id=pk)
models.Model.save_base(instance, raw=True) models.Model.save_base(instance, raw=True)
instance.data.set(data) instance.data.set_base(data, raw=True)
return [instance] return [instance]