summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Gibbons <1726961+marcgibbons@users.noreply.github.com>2025-12-08 09:42:43 -0500
committerNatalia <124304+nessita@users.noreply.github.com>2025-12-15 19:00:08 -0300
commitbe26ac85fdf06a7a8a655f6e7000b1263890717d (patch)
tree6e980533be257c369347ec335c35e74f195f45e2
parent6d4ccca48bd399e2e4a440b03d8d85ba5f2ea1ba (diff)
[6.0.x] Fixed #36783 -- Ensured proper handling of multi-value QueryDicts in querystring template tag.
Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com> Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Backport of 922c4cf972e04b1ce7ecee592231106724dcfd09 from main.
-rw-r--r--django/template/defaulttags.py10
-rw-r--r--docs/releases/6.0.1.txt4
-rw-r--r--tests/template_tests/syntax_tests/test_querystring.py54
3 files changed, 65 insertions, 3 deletions
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index ac3d5de901..3348e64263 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -1296,6 +1296,10 @@ def querystring(context, *args, **kwargs):
Keyword arguments are treated as an extra, final mapping. These mappings
are processed sequentially, with later arguments taking precedence.
+ Passing `None` as a value removes the corresponding key from the result.
+ For iterable values, `None` entries are ignored, but if all values are
+ `None`, the key is removed.
+
A query string prefixed with `?` is returned.
Raise TemplateSyntaxError if a positional argument is not a mapping or if
@@ -1327,7 +1331,8 @@ def querystring(context, *args, **kwargs):
"querystring requires mappings for positional arguments (got "
"%r instead)." % d
)
- for key, value in d.items():
+ items = d.lists() if isinstance(d, QueryDict) else d.items()
+ for key, value in items:
if not isinstance(key, str):
raise TemplateSyntaxError(
"querystring requires strings for mapping keys (got %r "
@@ -1336,7 +1341,8 @@ def querystring(context, *args, **kwargs):
if value is None:
params.pop(key, None)
elif isinstance(value, Iterable) and not isinstance(value, str):
- params.setlist(key, value)
+ # Drop None values; if no values remain, the key is removed.
+ params.setlist(key, [v for v in value if v is not None])
else:
params[key] = value
query_string = params.urlencode() if params else ""
diff --git a/docs/releases/6.0.1.txt b/docs/releases/6.0.1.txt
index 946102030c..72c35b3e69 100644
--- a/docs/releases/6.0.1.txt
+++ b/docs/releases/6.0.1.txt
@@ -9,4 +9,6 @@ Django 6.0.1 fixes several bugs in 6.0.
Bugfixes
========
-* ...
+* Fixed a regression in Django 6.0 where :ttag:`querystring` mishandled
+ multi-value :class:`~django.http.QueryDict` keys, both by only preserving the
+ last value and by incorrectly handling ``None`` values (:ticket:`36783`).
diff --git a/tests/template_tests/syntax_tests/test_querystring.py b/tests/template_tests/syntax_tests/test_querystring.py
index af8dbde955..8e0722f0fa 100644
--- a/tests/template_tests/syntax_tests/test_querystring.py
+++ b/tests/template_tests/syntax_tests/test_querystring.py
@@ -58,6 +58,22 @@ class QueryStringTagTests(SimpleTestCase):
context = RequestContext(request)
self.assertRenderEqual("querystring_multiple", context, expected="?x=y&amp;a=b")
+ @setup({"querystring_multiple_lists": "{% querystring %}"})
+ def test_querystring_multiple_lists(self):
+ request = self.request_factory.get("/", {"x": ["y", "z"], "a": ["b", "c"]})
+ context = RequestContext(request)
+ expected = "?x=y&amp;x=z&amp;a=b&amp;a=c"
+ self.assertRenderEqual("querystring_multiple_lists", context, expected=expected)
+
+ @setup({"querystring_lists_with_replacement": "{% querystring a=1 %}"})
+ def test_querystring_lists_with_replacement(self):
+ request = self.request_factory.get("/", {"x": ["y", "z"], "a": ["b", "c"]})
+ context = RequestContext(request)
+ expected = "?x=y&amp;x=z&amp;a=1"
+ self.assertRenderEqual(
+ "querystring_lists_with_replacement", context, expected=expected
+ )
+
@setup({"querystring_empty_params": "{% querystring qd %}"})
def test_querystring_empty_params(self):
cases = [{}, QueryDict()]
@@ -111,6 +127,44 @@ class QueryStringTagTests(SimpleTestCase):
context = RequestContext(request, {"my_dict": {"test": None}})
self.assertRenderEqual("querystring_remove_dict", context, expected="?a=1")
+ @setup({"querystring_remove_querydict": "{% querystring request my_query_dict %}"})
+ def test_querystring_remove_querydict(self):
+ request = self.request_factory.get("/", {"x": "1"})
+ my_qd = QueryDict(mutable=True)
+ my_qd["x"] = None
+ context = RequestContext(
+ request, {"request": request.GET, "my_query_dict": my_qd}
+ )
+ self.assertRenderEqual("querystring_remove_querydict", context, expected="?")
+
+ @setup(
+ {"querystring_remove_querydict_many": "{% querystring request my_query_dict %}"}
+ )
+ def test_querystring_remove_querydict_many(self):
+ request = self.request_factory.get(
+ "/", {"test": ["value1", "value2"], "a": [1, 2]}
+ )
+
+ qd_none = QueryDict(mutable=True)
+ qd_none["test"] = None
+
+ qd_list_none = QueryDict(mutable=True)
+ qd_list_none.setlist("test", [None, None])
+
+ qd_empty_list = QueryDict(mutable=True)
+ qd_empty_list.setlist("test", [])
+
+ for qd in (qd_none, qd_list_none, qd_empty_list):
+ with self.subTest(my_query_dict=qd):
+ context = RequestContext(
+ request, {"request": request.GET, "my_query_dict": qd}
+ )
+ self.assertRenderEqual(
+ "querystring_remove_querydict_many",
+ context,
+ expected="?a=1&amp;a=2",
+ )
+
@setup({"querystring_variable": "{% querystring a=a %}"})
def test_querystring_variable(self):
request = self.request_factory.get("/")