diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 52aae9e58933..3aa937c787a6 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -25,7 +25,7 @@
from django.urls import NoReverseMatch
from django.utils import formats, timezone
from django.utils.html import format_html
-from django.utils.safestring import mark_safe
+from django.utils.safestring import SafeString, mark_safe
from django.utils.text import capfirst
from django.utils.translation import gettext as _
@@ -103,11 +103,11 @@ def result_headers(cl):
if field_name == "action_checkbox":
aria_label = _("Select all objects on this page for an action")
yield {
- "text": mark_safe(
+ "text": SafeString(
f''
),
- "class_attrib": mark_safe(' class="action-checkbox-column"'),
+ "class_attrib": SafeString(' class="action-checkbox-column"'),
"sortable": False,
}
continue
@@ -256,7 +256,7 @@ def link_in_col(is_first, field_name, cl):
f, (models.DateField, models.TimeField, models.ForeignKey)
):
row_classes.append("nowrap")
- row_class = mark_safe(' class="%s"' % " ".join(row_classes))
+ row_class = SafeString(' class="%s"' % " ".join(row_classes))
# If list_display_links not defined, add the link tag to the first
# field
if link_to_changelist:
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py
index cc9caf9a82b9..12bd497ac663 100644
--- a/django/core/serializers/base.py
+++ b/django/core/serializers/base.py
@@ -290,11 +290,11 @@ def __repr__(self):
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 4728233a6a15..40ad8b260fe6 100644
--- a/django/db/models/fields/related_descriptors.py
+++ b/django/db/models/fields/related_descriptors.py
@@ -1262,15 +1262,16 @@ def count(self):
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 add(self, *objs, through_defaults=None):
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 @@ async def aadd(self, *objs, through_defaults=None):
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 @@ async def aremove(self, *objs):
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 clear(self):
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 clear(self):
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 @@ async def aclear(self):
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 set(self, objs, *, clear=False, through_defaults=None):
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 _get_add_plan(self, db, source_field_name):
)
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 _add_items(
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 _add_items(
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 _add_items(
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 _remove_items(self, source_field_name, target_field_name, *objs):
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 _remove_items(self, source_field_name, target_field_name, *objs):
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 _remove_items(self, source_field_name, target_field_name, *objs):
model=self.model,
pk_set=old_ids,
using=db,
+ raw=raw,
)
return ManyRelatedManager
diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py
index 42c2684d7cb1..b95fb2110214 100644
--- a/django/utils/dateparse.py
+++ b/django/utils/dateparse.py
@@ -40,6 +40,7 @@
iso8601_duration_re = _lazy_re_compile(
r"^(?P[-+]?)"
r"P"
+ r"(?:(?P\d+([.,]\d+)?)W)?"
r"(?:(?P\d+([.,]\d+)?)D)?"
r"(?:T"
r"(?:(?P\d+([.,]\d+)?)H)?"
@@ -134,8 +135,8 @@ def parse_duration(value):
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
- Also supports ISO 8601 representation and PostgreSQL's day-time interval
- format.
+ Also supports ISO 8601 representation (excluding years and months) and
+ PostgreSQL's day-time interval format.
"""
match = (
standard_duration_re.match(value)
diff --git a/django/utils/html.py b/django/utils/html.py
index 68260af3372e..71122ac79e8c 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -397,7 +397,7 @@ def handle_word(
attrs=nofollow_attr,
url=trimmed,
)
- return mark_safe(f"{lead}{middle}{trail}")
+ return SafeString(f"{lead}{middle}{trail}")
else:
if safe_input:
return mark_safe(word)
diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt
index 44958dcef3e8..aecd7f9a4419 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::
@@ -357,6 +366,8 @@ Argument Value
``pk_set`` ``{t.id}`` (since only ``Topping t`` was added to the relation)
``using`` ``"default"`` (since the default router sends writes here)
+
+``raw`` ``False`` (since it is not loaded from a fixture)
============== ============================================================
And if we would then do something like this:
@@ -387,6 +398,8 @@ Argument Value
relation)
``using`` ``"default"`` (since the default router sends writes here)
+
+``raw`` ``False`` (since it is not loaded from a fixture)
============== ============================================================
``class_prepared``
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index 69af23ad8345..1c42784d1394 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -147,6 +147,15 @@ The functions defined in this module share the following properties:
``P4DT1H15M20S`` which is equivalent to ``4 1:15:20``) or PostgreSQL's
day-time interval format (e.g. ``3 days 04:05:06``).
+ .. admonition:: Years and months not supported
+
+ ISO 8601 years (``Y``) and months (``M``) are unsupported as they
+ cannot be represented by :class:`~datetime.timedelta`.
+
+ .. versionchanged:: 6.1
+
+ ISO 8601 duration parsing now supports weeks (``PnW``).
+
``django.utils.decorators``
===========================
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index a392b3d75560..b72d6f3f0a2b 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -237,6 +237,10 @@ Management Commands
``suggest_on_error`` argument to ``True`` by default on Python 3.14, enabling
suggestions for mistyped subcommand names and argument choices.
+* The :djadmin:`loaddata` command now calls
+ :data:`~django.db.models.signals.m2m_changed` signals with ``raw=True`` when
+ loading fixtures.
+
Migrations
~~~~~~~~~~
@@ -321,7 +325,8 @@ URLs
Utilities
~~~~~~~~~
-* ...
+* :func:`~django.utils.dateparse.parse_duration` now supports ISO 8601
+ durations expressed in weeks (``PnW``).
Validators
~~~~~~~~~~
diff --git a/docs/topics/db/fixtures.txt b/docs/topics/db/fixtures.txt
index 6066d34f8e66..9e91e0d8450a 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 488c0a6fbe77..980771d54bf1 100644
--- a/tests/m2m_signals/tests.py
+++ b/tests/m2m_signals/tests.py
@@ -35,6 +35,7 @@ def m2m_changed_signal_receiver(self, signal, sender, **kwargs):
"action": kwargs["action"],
"reverse": kwargs["reverse"],
"model": kwargs["model"],
+ "raw": kwargs["raw"],
}
if kwargs["pk_set"]:
message["objects"] = list(
@@ -114,6 +115,7 @@ def test_m2m_relations_add_remove_clear(self):
"action": "pre_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors, self.engine, self.wheelset],
}
)
@@ -123,6 +125,7 @@ def test_m2m_relations_add_remove_clear(self):
"action": "post_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors, self.engine, self.wheelset],
}
)
@@ -136,6 +139,7 @@ def test_m2m_relations_add_remove_clear(self):
"action": "pre_add",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [self.bmw, self.toyota],
}
)
@@ -145,6 +149,7 @@ def test_m2m_relations_add_remove_clear(self):
"action": "post_add",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [self.bmw, self.toyota],
}
)
@@ -163,6 +168,7 @@ def test_m2m_relations_signals_remove_relation(self):
"action": "pre_remove",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.airbag, self.engine],
},
{
@@ -170,6 +176,7 @@ def test_m2m_relations_signals_remove_relation(self):
"action": "post_remove",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.airbag, self.engine],
},
],
@@ -188,6 +195,7 @@ def test_m2m_relations_signals_give_the_self_vw_some_optional_parts(self):
"action": "pre_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.airbag, self.sunroof],
}
)
@@ -197,6 +205,7 @@ def test_m2m_relations_signals_give_the_self_vw_some_optional_parts(self):
"action": "post_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.airbag, self.sunroof],
}
)
@@ -210,6 +219,7 @@ def test_m2m_relations_signals_give_the_self_vw_some_optional_parts(self):
"action": "pre_add",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [self.bmw, self.toyota],
}
)
@@ -219,6 +229,7 @@ def test_m2m_relations_signals_give_the_self_vw_some_optional_parts(self):
"action": "post_add",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [self.bmw, self.toyota],
}
)
@@ -237,6 +248,7 @@ def test_m2m_relations_signals_reverse_relation_with_custom_related_name(self):
"action": "pre_remove",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [self.vw],
},
{
@@ -244,6 +256,7 @@ def test_m2m_relations_signals_reverse_relation_with_custom_related_name(self):
"action": "post_remove",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [self.vw],
},
],
@@ -261,12 +274,14 @@ def test_m2m_relations_signals_clear_all_parts_of_the_self_vw(self):
"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 @@ def test_m2m_relations_signals_all_the_doors_off_of_cars(self):
"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 @@ def test_m2m_relations_signals_reverse_relation(self):
"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 @@ def test_m2m_relations_signals_alternative_ways(self):
"action": "pre_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [p6],
}
)
@@ -339,6 +359,7 @@ def test_m2m_relations_signals_alternative_ways(self):
"action": "post_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [p6],
}
)
@@ -352,6 +373,7 @@ def test_m2m_relations_signals_alternative_ways(self):
"action": "pre_remove",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [p6],
}
)
@@ -361,6 +383,7 @@ def test_m2m_relations_signals_alternative_ways(self):
"action": "post_remove",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [p6],
}
)
@@ -370,6 +393,7 @@ def test_m2m_relations_signals_alternative_ways(self):
"action": "pre_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors, self.engine, self.wheelset],
}
)
@@ -379,6 +403,7 @@ def test_m2m_relations_signals_alternative_ways(self):
"action": "post_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors, self.engine, self.wheelset],
}
)
@@ -397,6 +422,7 @@ def test_m2m_relations_signals_clearing_removing(self):
"action": "pre_clear",
"reverse": False,
"model": Part,
+ "raw": False,
}
)
expected_messages.append(
@@ -405,6 +431,7 @@ def test_m2m_relations_signals_clearing_removing(self):
"action": "post_clear",
"reverse": False,
"model": Part,
+ "raw": False,
}
)
expected_messages.append(
@@ -413,6 +440,7 @@ def test_m2m_relations_signals_clearing_removing(self):
"action": "pre_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors, self.engine, self.wheelset],
}
)
@@ -422,6 +450,7 @@ def test_m2m_relations_signals_clearing_removing(self):
"action": "post_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors, self.engine, self.wheelset],
}
)
@@ -435,6 +464,7 @@ def test_m2m_relations_signals_clearing_removing(self):
"action": "pre_remove",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.engine],
}
)
@@ -444,6 +474,7 @@ def test_m2m_relations_signals_clearing_removing(self):
"action": "post_remove",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.engine],
}
)
@@ -464,6 +495,7 @@ def test_m2m_relations_signals_when_inheritance(self):
"action": "pre_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors],
}
)
@@ -473,6 +505,7 @@ def test_m2m_relations_signals_when_inheritance(self):
"action": "post_add",
"reverse": False,
"model": Part,
+ "raw": False,
"objects": [self.doors],
}
)
@@ -485,6 +518,7 @@ def test_m2m_relations_signals_when_inheritance(self):
"action": "pre_add",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [c4b],
}
)
@@ -494,6 +528,7 @@ def test_m2m_relations_signals_when_inheritance(self):
"action": "post_add",
"reverse": True,
"model": Car,
+ "raw": False,
"objects": [c4b],
}
)
@@ -519,6 +554,7 @@ def test_m2m_relations_with_self_add_friends(self):
"action": "pre_add",
"reverse": False,
"model": Person,
+ "raw": False,
"objects": [self.bob, self.chuck],
},
{
@@ -526,6 +562,7 @@ def test_m2m_relations_with_self_add_friends(self):
"action": "post_add",
"reverse": False,
"model": Person,
+ "raw": False,
"objects": [self.bob, self.chuck],
},
],
@@ -542,6 +579,7 @@ def test_m2m_relations_with_self_add_fan(self):
"action": "pre_add",
"reverse": False,
"model": Person,
+ "raw": False,
"objects": [self.daisy],
},
{
@@ -549,6 +587,7 @@ def test_m2m_relations_with_self_add_fan(self):
"action": "post_add",
"reverse": False,
"model": Person,
+ "raw": False,
"objects": [self.daisy],
},
],
@@ -565,6 +604,7 @@ def test_m2m_relations_with_self_add_idols(self):
"action": "pre_add",
"reverse": True,
"model": Person,
+ "raw": False,
"objects": [self.alice, self.bob],
},
{
@@ -572,6 +612,89 @@ def test_m2m_relations_with_self_add_idols(self):
"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 c626f2550ae8..121404a0ef46 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]
diff --git a/tests/utils_tests/test_dateparse.py b/tests/utils_tests/test_dateparse.py
index a01942bfd03c..b37cc9544a43 100644
--- a/tests/utils_tests/test_dateparse.py
+++ b/tests/utils_tests/test_dateparse.py
@@ -194,7 +194,11 @@ def test_iso_8601(self):
test_values = (
("P4Y", None),
("P4M", None),
- ("P4W", None),
+ ("P4W", timedelta(weeks=4)),
+ ("P0.5W", timedelta(weeks=0.5)),
+ ("P0,5W", timedelta(weeks=0.5)),
+ ("-P0.5W", timedelta(weeks=-0.5)),
+ ("P1W1D", timedelta(weeks=1, days=1)),
("P4D", timedelta(days=4)),
("-P1D", timedelta(days=-1)),
("P0.5D", timedelta(hours=12)),