1
0
mirror of https://github.com/django/django.git synced 2025-06-05 03:29:12 +00:00

Fixed #35529 -- Added support for positional arguments in querystring template tag.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
This commit is contained in:
Giannis Terzopoulos 2025-03-19 12:21:41 +01:00 committed by nessita
parent 9608678704
commit a39c28706a
5 changed files with 168 additions and 26 deletions

View File

@ -391,6 +391,7 @@ answer newbie questions, and generally made Django that much better:
Georg "Hugo" Bauer <gb@hugo.westfalen.de> Georg "Hugo" Bauer <gb@hugo.westfalen.de>
Georgi Stanojevski <glisha@gmail.com> Georgi Stanojevski <glisha@gmail.com>
Gerardo Orozco <gerardo.orozco.mosqueda@gmail.com> Gerardo Orozco <gerardo.orozco.mosqueda@gmail.com>
Giannis Terzopoulos <terzo.giannis@gmail.com>
Gil Gonçalves <lursty@gmail.com> Gil Gonçalves <lursty@gmail.com>
Girish Kumar <girishkumarkh@gmail.com> Girish Kumar <girishkumarkh@gmail.com>
Girish Sontakke <girishsontakke7@gmail.com> Girish Sontakke <girishsontakke7@gmail.com>

View File

@ -4,12 +4,13 @@ import re
import sys import sys
import warnings import warnings
from collections import namedtuple from collections import namedtuple
from collections.abc import Iterable from collections.abc import Iterable, Mapping
from datetime import datetime from datetime import datetime
from itertools import cycle as itertools_cycle from itertools import cycle as itertools_cycle
from itertools import groupby from itertools import groupby
from django.conf import settings from django.conf import settings
from django.http import QueryDict
from django.utils import timezone from django.utils import timezone
from django.utils.html import conditional_escape, escape, format_html from django.utils.html import conditional_escape, escape, format_html
from django.utils.lorem_ipsum import paragraphs, words from django.utils.lorem_ipsum import paragraphs, words
@ -1173,17 +1174,23 @@ def now(parser, token):
@register.simple_tag(name="querystring", takes_context=True) @register.simple_tag(name="querystring", takes_context=True)
def querystring(context, query_dict=None, **kwargs): def querystring(context, *args, **kwargs):
""" """
Build a query string using `query_dict` and `kwargs` arguments. Build a query string using `args` and `kwargs` arguments.
This tag constructs a new query string by adding, removing, or modifying This tag constructs a new query string by adding, removing, or modifying
parameters, starting from the given `query_dict` (defaulting to parameters from the given positional and keyword arguments. Positional
`request.GET`). Keyword arguments are processed sequentially, with later arguments must be mappings (such as `QueryDict` or `dict`), and
arguments taking precedence. `request.GET` is used as the starting point if `args` is empty.
Keyword arguments are treated as an extra, final mapping. These mappings
are processed sequentially, with later arguments taking precedence.
A query string prefixed with `?` is returned. A query string prefixed with `?` is returned.
Raise TemplateSyntaxError if a positional argument is not a mapping or if
keys are not strings.
For example:: For example::
{# Set a parameter on top of `request.GET` #} {# Set a parameter on top of `request.GET` #}
@ -1197,18 +1204,31 @@ def querystring(context, query_dict=None, **kwargs):
{# Use a custom ``QueryDict`` #} {# Use a custom ``QueryDict`` #}
{% querystring my_query_dict foo=3 %} {% querystring my_query_dict foo=3 %}
{# Use multiple positional and keyword arguments #}
{% querystring my_query_dict my_dict foo=3 bar=None %}
""" """
if query_dict is None: if not args:
query_dict = context.request.GET args = [context.request.GET]
params = query_dict.copy() params = QueryDict(mutable=True)
for key, value in kwargs.items(): for d in [*args, kwargs]:
if value is None: if not isinstance(d, Mapping):
if key in params: raise TemplateSyntaxError(
del params[key] "querystring requires mappings for positional arguments (got "
elif isinstance(value, Iterable) and not isinstance(value, str): "%r instead)." % d
params.setlist(key, value) )
else: for key, value in d.items():
params[key] = value if not isinstance(key, str):
raise TemplateSyntaxError(
"querystring requires strings for mapping keys (got %r "
"instead)." % key
)
if value is None:
params.pop(key, None)
elif isinstance(value, Iterable) and not isinstance(value, str):
params.setlist(key, value)
else:
params[key] = value
query_string = params.urlencode() if params else "" query_string = params.urlencode() if params else ""
return f"?{query_string}" return f"?{query_string}"

View File

@ -964,8 +964,14 @@ output (as a string) inside a variable. This is useful if you want to use
Outputs a URL-encoded formatted query string based on the provided parameters. Outputs a URL-encoded formatted query string based on the provided parameters.
This tag requires a :class:`~django.http.QueryDict` instance, which defaults to This tag accepts positional arguments, which must be mappings (such as
:attr:`request.GET <django.http.HttpRequest.GET>` if none is provided. :class:`~django.http.QueryDict` or :class:`dict`). If no positional arguments
are provided, :attr:`request.GET <django.http.HttpRequest.GET>` is used as the
default to construct the query string.
Positional arguments are processed sequentially, while keyword arguments are
treated as key-value pairs, applied last. Later arguments take precedence over
earlier ones, ensuring the most recent pairs are reflected in the final result.
The result always includes a leading ``"?"`` since this tag is mainly used for 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. links, and an empty result could prevent the page from reloading as expected.
@ -1033,16 +1039,33 @@ Handling lists
If ``my_list`` is ``["red", "blue"]``, the output will be If ``my_list`` is ``["red", "blue"]``, the output will be
``?color=red&color=blue``, preserving the list structure in the query string. ``?color=red&color=blue``, preserving the list structure in the query string.
Custom QueryDict Customizing the base QueryDict
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can pass custom ``QueryDict`` or ``dict`` instances as positional arguments
to replace ``request.GET``. When multiple arguments are provided, key-value
pairs from later arguments take precedence over earlier ones.
For example, if ``my_query_dict`` is ``<QueryDict: {'color': ['blue'], 'size':
['S']}>`` and ``my_dict`` is ``{'color': 'orange', 'fabric': 'silk', 'type':
'dress'}``, this outputs ``?color=orange&size=S&fabric=silk``.
.. code-block:: html+django .. code-block:: html+django
{% querystring my_query_dict %} {% querystring my_query_dict my_dict size="S" type=None %}
You can provide a custom ``QueryDict`` to be used instead of ``request.GET``. If all keys are removed by setting them to ``None``, this outputs ``?``:
So if ``my_query_dict`` is ``<QueryDict: {'color': ['blue']}>``, this outputs
``?color=blue``. If ``my_query_dict`` is empty, the output will be ``?``. .. code-block:: html+django
{% querystring my_query_dict my_dict color=None size=None fabric=None type=None %}
Similarly, if all positional arguments are empty and keyword arguments do not
contribute any new params, the output will also be ``?``.
.. versionchanged:: 6.0
Support for multiple positional mapping arguments was added.
Dynamic usage Dynamic usage
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -236,6 +236,10 @@ Templates
* The :ttag:`querystring` template tag now consistently prefixes the returned * The :ttag:`querystring` template tag now consistently prefixes the returned
query string with a ``?``, ensuring reliable link generation behavior. query string with a ``?``, ensuring reliable link generation behavior.
* The :ttag:`querystring` template tag now accepts multiple positional
arguments, which must be mappings, such as :class:`~django.http.QueryDict`
or :class:`dict`.
Tests Tests
~~~~~ ~~~~~

View File

@ -1,5 +1,6 @@
from django.http import QueryDict from django.http import QueryDict
from django.template import RequestContext from django.template import RequestContext
from django.template.base import TemplateSyntaxError
from django.test import RequestFactory, SimpleTestCase from django.test import RequestFactory, SimpleTestCase
from ..utils import setup from ..utils import setup
@ -13,6 +14,10 @@ class QueryStringTagTests(SimpleTestCase):
output = self.engine.render_to_string(template_name, context) output = self.engine.render_to_string(template_name, context)
self.assertEqual(output, expected) self.assertEqual(output, expected)
def assertTemplateSyntaxError(self, template_name, context, expected):
with self.assertRaisesMessage(TemplateSyntaxError, expected):
self.engine.render_to_string(template_name, context)
@setup({"querystring_empty_get_params": "{% querystring %}"}) @setup({"querystring_empty_get_params": "{% querystring %}"})
def test_querystring_empty_get_params(self): def test_querystring_empty_get_params(self):
context = RequestContext(self.request_factory.get("/")) context = RequestContext(self.request_factory.get("/"))
@ -26,6 +31,19 @@ class QueryStringTagTests(SimpleTestCase):
with self.subTest(context=context): with self.subTest(context=context):
self.assertRenderEqual("querystring_remove_all_params", context, "?") self.assertRenderEqual("querystring_remove_all_params", context, "?")
@setup(
{
"querystring_remove_all_params_custom_querydict": (
"{% querystring my_query_dict my_dict a=None %}"
)
}
)
def test_querystring_remove_all_params_custom_querydict(self):
context = {"my_query_dict": QueryDict("a=1&b=2"), "my_dict": {"b": None}}
self.assertRenderEqual(
"querystring_remove_all_params_custom_querydict", context, "?"
)
@setup({"querystring_non_empty_get_params": "{% querystring %}"}) @setup({"querystring_non_empty_get_params": "{% querystring %}"})
def test_querystring_non_empty_get_params(self): def test_querystring_non_empty_get_params(self):
request = self.request_factory.get("/", {"a": "b"}) request = self.request_factory.get("/", {"a": "b"})
@ -42,7 +60,7 @@ class QueryStringTagTests(SimpleTestCase):
@setup({"querystring_empty_params": "{% querystring qd %}"}) @setup({"querystring_empty_params": "{% querystring qd %}"})
def test_querystring_empty_params(self): def test_querystring_empty_params(self):
cases = [None, {}, QueryDict()] cases = [{}, QueryDict()]
request = self.request_factory.get("/") request = self.request_factory.get("/")
qs = "?a=b" qs = "?a=b"
request_with_qs = self.request_factory.get(f"/{qs}") request_with_qs = self.request_factory.get(f"/{qs}")
@ -87,6 +105,82 @@ class QueryStringTagTests(SimpleTestCase):
"querystring_remove_nonexistent", context, expected="?x=y&amp;a=1" "querystring_remove_nonexistent", context, expected="?x=y&amp;a=1"
) )
@setup({"querystring_remove_dict": "{% querystring my_dict a=1 %}"})
def test_querystring_remove_from_dict(self):
request = self.request_factory.get("/", {"test": "value"})
context = RequestContext(request, {"my_dict": {"test": None}})
self.assertRenderEqual("querystring_remove_dict", context, expected="?a=1")
@setup({"querystring_variable": "{% querystring a=a %}"})
def test_querystring_variable(self):
request = self.request_factory.get("/")
context = RequestContext(request, {"a": 1})
self.assertRenderEqual("querystring_variable", context, expected="?a=1")
@setup({"querystring_dict": "{% querystring my_dict %}"})
def test_querystring_dict(self):
context = {"my_dict": {"a": 1}}
self.assertRenderEqual("querystring_dict", context, expected="?a=1")
@setup({"querystring_dict_list": "{% querystring my_dict %}"})
def test_querystring_dict_list_values(self):
context = {"my_dict": {"a": [1, 2]}}
self.assertRenderEqual(
"querystring_dict_list", context, expected="?a=1&amp;a=2"
)
@setup(
{
"querystring_multiple_args_override": (
"{% querystring my_dict my_query_dict x=3 y=None %}"
)
}
)
def test_querystring_multiple_args_override(self):
context = {"my_dict": {"x": 0, "y": 42}, "my_query_dict": QueryDict("a=1&b=2")}
self.assertRenderEqual(
"querystring_multiple_args_override",
context,
expected="?x=3&amp;a=1&amp;b=2",
)
@setup({"querystring_request_get_ignored": "{% querystring my_mapping %}"})
def test_querystring_request_get_ignored(self):
cases = [({"y": "x"}, "?y=x"), ({}, "?")]
request = self.request_factory.get("/", {"x": "y", "a": "b"})
for param, expected in cases:
with self.subTest(param=param):
context = RequestContext(request, {"my_mapping": param})
self.assertRenderEqual(
"querystring_request_get_ignored", context, expected=expected
)
@setup({"querystring_same_arg": "{% querystring a=1 a=2 %}"})
def test_querystring_same_arg(self):
msg = "'querystring' received multiple values for keyword argument 'a'"
self.assertTemplateSyntaxError("querystring_same_arg", {}, msg)
@setup({"querystring_non_mapping_args": "{% querystring somevar %}"})
def test_querystring_non_mapping_args(self):
cases = [None, 0, "", []]
request = self.request_factory.get("/")
msg = (
"querystring requires mappings for positional arguments (got %r "
"instead)."
)
for param in cases:
with self.subTest(param=param):
context = RequestContext(request, {"somevar": param})
self.assertTemplateSyntaxError(
"querystring_non_mapping_args", context, msg % param
)
@setup({"querystring_non_string_dict_keys": "{% querystring my_dict %}"})
def test_querystring_non_string_dict_keys(self):
context = {"my_dict": {0: 1}}
msg = "querystring requires strings for mapping keys (got 0 instead)."
self.assertTemplateSyntaxError("querystring_non_string_dict_keys", context, msg)
@setup({"querystring_list": "{% querystring a=my_list %}"}) @setup({"querystring_list": "{% querystring a=my_list %}"})
def test_querystring_add_list(self): def test_querystring_add_list(self):
request = self.request_factory.get("/") request = self.request_factory.get("/")