1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

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

This commit is contained in:
Giannis Terzopoulos 2024-12-02 16:38:22 +01:00 committed by Natalia
parent 7e41a7a47d
commit 33ee6b23c7
5 changed files with 135 additions and 30 deletions

View File

@ -387,6 +387,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
@ -1170,11 +1171,13 @@ 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):
""" """
Add, remove, and change parameters of a ``QueryDict`` and return the result Modify and return a query string with updated parameters.
as a query string. If the ``query_dict`` argument is not provided, default
to ``request.GET``. Add, remove, or change parameters in a given `QueryDict` or `dict`,
returning the result as a query string. Any arguments provided will
override existing keys. If no arguments are given, `request.GET` is used.
For example:: For example::
@ -1188,21 +1191,44 @@ def querystring(context, query_dict=None, **kwargs):
{% querystring page=page_obj.next_page_number %} {% querystring page=page_obj.next_page_number %}
A custom ``QueryDict`` can also be used:: A custom `QueryDict` can be used::
{% querystring my_query_dict foo=3 %} {% querystring my_query_dict foo=3 %}
Or a `dict` can be used::
{% querystring my_dict foo=3 %}
As can a mix of multiple args and kwargs::
{% querystring my_dict my_query_dict foo=3 bar=None %}
""" """
if query_dict is None: try:
query_dict = context.request.GET query_dict = context.request.GET.copy()
query_dict = query_dict.copy() except AttributeError:
for key, value in kwargs.items(): if not args:
if value is None: raise
if key in query_dict: query_dict = QueryDict(mutable=True)
del query_dict[key] for d in args + (kwargs,):
elif isinstance(value, Iterable) and not isinstance(value, str): if d is None:
query_dict.setlist(key, value) continue
else: if not isinstance(d, Mapping):
query_dict[key] = value raise TemplateSyntaxError(
"querystring requires mappings for positional arguments (received %r "
"instead)." % d
)
for key, value in d.items():
if not isinstance(key, str):
raise TemplateSyntaxError(
"querystring requires strings for mapping keys (received %r "
"instead)." % key
)
if value is None:
query_dict.pop(key, None)
elif isinstance(value, Iterable) and not isinstance(value, str):
query_dict.setlist(key, value)
else:
query_dict[key] = value
if not query_dict: if not query_dict:
return "" return ""
query_string = query_dict.urlencode() query_string = query_dict.urlencode()

View File

@ -961,20 +961,20 @@ 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 requires a :class:`~django.http.QueryDict` or :class:`dict` instance,
:attr:`request.GET <django.http.HttpRequest.GET>` if none is provided. which defaults to :attr:`request.GET <django.http.HttpRequest.GET>` if none is
provided.
If the :class:`~django.http.QueryDict` is empty and no additional parameters A non-empty result includes a leading ``"?"``, whereas an empty result returns
are provided, an empty string is returned. A non-empty result includes a an empty string.
leading ``"?"``.
.. admonition:: Using ``request.GET`` as default .. admonition:: Using ``request.GET`` as default
To use ``request.GET`` as the default ``QueryDict`` instance, the To use ``request.GET`` as the default ``QueryDict`` instance, the
``django.template.context_processors.request`` context processor should be ``django.template.context_processors.request`` context processor should be
enabled. If it's not enabled, you must either explicitly pass the enabled. If it's not enabled, you must either explicitly pass the
``request`` object into the template context, or provide a ``QueryDict`` ``request`` object into the template context, or pass the ``request.GET``
instance to this tag. ``QueryDict`` instance to this tag.
Basic usage Basic usage
~~~~~~~~~~~ ~~~~~~~~~~~
@ -993,16 +993,24 @@ Outputs the current query string verbatim. So if the query string is
Outputs the current query string with the addition of the ``size`` parameter. Outputs the current query string with the addition of the ``size`` parameter.
Following the previous example, the output would be ``?color=green&size=M``. Following the previous example, the output would be ``?color=green&size=M``.
Custom QueryDict Customizing the default QueryDict
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: html+django .. code-block:: html+django
{% querystring my_query_dict %} {% querystring my_query_dict my_dict %}
You can provide a custom ``QueryDict`` to be used instead of ``request.GET``. You can provide custom ``QueryDict`` or ``dict`` instances as positional
So if ``my_query_dict`` is ``<QueryDict: {'color': ['blue']}>``, this outputs arguments to be used instead of ``request.GET``. Key values from later
``?color=blue``. arguments take precedence over previously seen keys. So if ``my_query_dict``
is ``<QueryDict: {'color': ['blue'], 'size': ['S']}>`` and ``my_dict`` is
``{'color': 'orange', 'fabric': 'silk'}``, this outputs
``?color=orange&size=S&fabric=silk``.
.. versionchanged:: 5.2
Support for multiple positional arguments and positional arguments of
type ``dict`` were added.
Setting items Setting items
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -360,6 +360,9 @@ Templates
* The new :meth:`~django.template.Library.simple_block_tag` decorator enables * The new :meth:`~django.template.Library.simple_block_tag` decorator enables
the creation of simple block tags, which can accept and use a section of the the creation of simple block tags, which can accept and use a section of the
template. template.
* The :ttag:`querystring` template tag now supports multiple positional
arguments, which must be mappings, such as :class:`~django.http.QueryDict`
or ``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({"test_querystring_empty_get_params": "{% querystring %}"}) @setup({"test_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("/"))
@ -65,6 +70,14 @@ class QueryStringTagTests(SimpleTestCase):
context = RequestContext(request) context = RequestContext(request)
self.assertRenderEqual("querystring_remove", context, expected="?a=1") self.assertRenderEqual("querystring_remove", context, expected="?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"})
template = self.engine.get_template("querystring_remove_dict")
context = RequestContext(request, {"my_dict": {"test": None}})
output = template.render(context)
self.assertEqual(output, "?a=1")
@setup({"querystring_remove_nonexistent": "{% querystring nonexistent=None a=1 %}"}) @setup({"querystring_remove_nonexistent": "{% querystring nonexistent=None a=1 %}"})
def test_querystring_remove_nonexistent(self): def test_querystring_remove_nonexistent(self):
request = self.request_factory.get("/", {"x": "y", "a": "1"}) request = self.request_factory.get("/", {"x": "y", "a": "1"})
@ -73,6 +86,60 @@ 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_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_variable": "{% querystring a=a %}"})
def test_querystring_variable(self):
request = self.request_factory.get("/")
template = self.engine.get_template("querystring_variable")
context = RequestContext(request, {"a": 1})
output = template.render(context)
self.assertEqual(output, "?a=1")
@setup({"querystring_dict": "{% querystring my_dict %}"})
def test_querystring_dict(self):
context = {"my_dict": {"a": 1}}
output = self.engine.render_to_string("querystring_dict", context)
self.assertEqual(output, "?a=1")
@setup({"querystring_dict_list": "{% querystring my_dict %}"})
def test_querystring_dict_list_values(self):
context = {"my_dict": {"a": [1, 2]}}
output = self.engine.render_to_string("querystring_dict_list", context)
self.assertEqual(output, "?a=1&amp;a=2")
@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 (received 0 instead)."
self.assertTemplateSyntaxError("querystring_non_string_dict_keys", context, msg)
@setup({"querystring_non_dict_args": "{% querystring somevar %}"})
def test_querystring_non_dict_args(self):
context = {"somevar": 0}
msg = (
"querystring requires mappings for positional arguments (received 0 "
"instead)."
)
self.assertTemplateSyntaxError("querystring_non_dict_args", context, msg)
@setup(
{
"querystring_multiple_args_override": (
"{% querystring my_dict my_query_dict x=3 %}"
)
}
)
def test_querystring_multiple_args_override(self):
context = {"my_dict": {"x": 0}, "my_query_dict": QueryDict("a=1&b=2")}
output = self.engine.render_to_string(
"querystring_multiple_args_override", context
)
self.assertEqual(output, "?x=3&amp;a=1&amp;b=2")
@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("/")