diff options
| author | Marc Gibbons <1726961+marcgibbons@users.noreply.github.com> | 2025-12-08 09:42:43 -0500 |
|---|---|---|
| committer | Natalia <124304+nessita@users.noreply.github.com> | 2025-12-15 19:00:08 -0300 |
| commit | be26ac85fdf06a7a8a655f6e7000b1263890717d (patch) | |
| tree | 6e980533be257c369347ec335c35e74f195f45e2 | |
| parent | 6d4ccca48bd399e2e4a440b03d8d85ba5f2ea1ba (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.py | 10 | ||||
| -rw-r--r-- | docs/releases/6.0.1.txt | 4 | ||||
| -rw-r--r-- | tests/template_tests/syntax_tests/test_querystring.py | 54 |
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&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&x=z&a=b&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&x=z&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&a=2", + ) + @setup({"querystring_variable": "{% querystring a=a %}"}) def test_querystring_variable(self): request = self.request_factory.get("/") |
