Fixed #36783 -- Ensured proper handling of multi-value QueryDicts in querystring template tag.
Some checks are pending
Docs / spelling (push) Waiting to run
Docs / blacken-docs (push) Waiting to run
Docs / lint-docs (push) Waiting to run
Linters / flake8 (push) Waiting to run
Linters / isort (push) Waiting to run
Linters / black (push) Waiting to run
Linters / zizmor (push) Waiting to run
Tests / Windows, SQLite, Python 3.14 (push) Waiting to run
Tests / JavaScript tests (push) Waiting to run

Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
This commit is contained in:
Marc Gibbons 2025-12-08 09:42:43 -05:00 committed by nessita
parent 0071cb1efd
commit 922c4cf972
3 changed files with 65 additions and 3 deletions

View file

@ -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 ""

View file

@ -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`).

View file

@ -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("/")