diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index a7885e541a..d26d54a826 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1182,6 +1182,8 @@ def querystring(context, query_dict=None, **kwargs): `request.GET`). Keyword arguments are processed sequentially, with later arguments taking precedence. + A query string prefixed with `?` is returned. + For example:: {# Set a parameter on top of `request.GET` #} @@ -1207,9 +1209,7 @@ def querystring(context, query_dict=None, **kwargs): params.setlist(key, value) else: params[key] = value - if not params and not query_dict: - return "" - query_string = params.urlencode() + query_string = params.urlencode() if params else "" return f"?{query_string}" diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 5712b65d54..849faaa46f 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -967,9 +967,8 @@ Outputs a URL-encoded formatted query string based on the provided parameters. This tag requires a :class:`~django.http.QueryDict` instance, which defaults to :attr:`request.GET ` if none is provided. -If the :class:`~django.http.QueryDict` is empty and no additional parameters -are provided, an empty string is returned. Otherwise, the result includes a -leading ``"?"``. +The result always includes a leading ``"?"`` since this tag is mainly used for +links, and an empty result could prevent the page from reloading as expected. .. admonition:: Using ``request.GET`` as default @@ -979,6 +978,10 @@ leading ``"?"``. ``request`` object into the template context, or provide a ``QueryDict`` instance to this tag. +.. versionchanged:: 6.0 + + A ``?`` was prepended to the query string for empty results. + Basic usage ~~~~~~~~~~~ @@ -986,8 +989,9 @@ Basic usage {% querystring %} -Outputs the current query string verbatim. So if the query string is -``?color=green``, the output would be ``?color=green``. +Outputs the current query string verbatim. So if the query string in the +request is ``?color=green``, the output would be ``?color=green``. If the +current query string is empty, the output will be ``?``. .. code-block:: html+django @@ -1038,7 +1042,7 @@ Custom QueryDict You can provide a custom ``QueryDict`` to be used instead of ``request.GET``. So if ``my_query_dict`` is ````, this outputs -``?color=blue``. +``?color=blue``. If ``my_query_dict`` is empty, the output will be ``?``. Dynamic usage ~~~~~~~~~~~~~ diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index f651274dfe..da69c73044 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -233,6 +233,9 @@ Templates * The new variable ``forloop.length`` is now available within a :ttag:`for` loop. +* The :ttag:`querystring` template tag now consistently prefixes the returned + query string with a ``?``, ensuring reliable link generation behavior. + Tests ~~~~~ diff --git a/tests/template_tests/syntax_tests/test_querystring.py b/tests/template_tests/syntax_tests/test_querystring.py index 4070df34b2..7b19bb11ad 100644 --- a/tests/template_tests/syntax_tests/test_querystring.py +++ b/tests/template_tests/syntax_tests/test_querystring.py @@ -16,17 +16,15 @@ class QueryStringTagTests(SimpleTestCase): @setup({"querystring_empty_get_params": "{% querystring %}"}) def test_querystring_empty_get_params(self): context = RequestContext(self.request_factory.get("/")) - self.assertRenderEqual("querystring_empty_get_params", context, expected="") + self.assertRenderEqual("querystring_empty_get_params", context, expected="?") @setup({"querystring_remove_all_params": "{% querystring a=None %}"}) def test_querystring_remove_all_params(self): non_empty_context = RequestContext(self.request_factory.get("/?a=b")) empty_context = RequestContext(self.request_factory.get("/")) - for context, expected in [(non_empty_context, "?"), (empty_context, "")]: - with self.subTest(expected=expected): - self.assertRenderEqual( - "querystring_remove_all_params", context, expected - ) + for context in [non_empty_context, empty_context]: + with self.subTest(context=context): + self.assertRenderEqual("querystring_remove_all_params", context, "?") @setup({"querystring_non_empty_get_params": "{% querystring %}"}) def test_querystring_non_empty_get_params(self): @@ -46,10 +44,20 @@ class QueryStringTagTests(SimpleTestCase): def test_querystring_empty_params(self): cases = [None, {}, QueryDict()] request = self.request_factory.get("/") + qs = "?a=b" + request_with_qs = self.request_factory.get(f"/{qs}") for param in cases: + # Empty `query_dict` and nothing on `request.GET`. with self.subTest(param=param): context = RequestContext(request, {"qd": param}) - self.assertRenderEqual("querystring_empty_params", context, expected="") + self.assertRenderEqual( + "querystring_empty_params", context, expected="?" + ) + # Empty `query_dict` and a query string in `request.GET`. + with self.subTest(param=param, qs=qs): + context = RequestContext(request_with_qs, {"qd": param}) + expected = "?" if param is not None else qs + self.assertRenderEqual("querystring_empty_params", context, expected) @setup({"querystring_replace": "{% querystring a=1 %}"}) def test_querystring_replace(self):