summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Carrick <tom@carrick.eu>2023-10-15 22:01:35 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-10-26 09:57:21 +0200
commite67d3580edbee1a4b58d40875293714ac3fc6937 (patch)
tree1299d0a74e11e3d5f6039459510cd12f73985078
parent718b32c6918037cfc746d7867333d79a3c887a8c (diff)
Fixed #10941 -- Added {% query_string %} template tag.
-rw-r--r--django/template/defaulttags.py41
-rw-r--r--docs/ref/templates/builtins.txt72
-rw-r--r--docs/releases/5.1.txt5
-rw-r--r--tests/template_tests/syntax_tests/test_query_string.py100
4 files changed, 218 insertions, 0 deletions
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 79c10232bb..188bdf8c05 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -10,6 +10,7 @@ from itertools import groupby
from django.conf import settings
from django.utils import timezone
from django.utils.html import conditional_escape, escape, format_html
+from django.utils.itercompat import is_iterable
from django.utils.lorem_ipsum import paragraphs, words
from django.utils.safestring import mark_safe
@@ -1167,6 +1168,46 @@ def now(parser, token):
return NowNode(format_string, asvar)
+@register.simple_tag(takes_context=True)
+def query_string(context, query_dict=None, **kwargs):
+ """
+ Add, remove, and change parameters of a ``QueryDict`` and return the result
+ as a query string. If the ``query_dict`` argument is not provided, default
+ to ``request.GET``.
+
+ For example::
+
+ {% query_string foo=3 %}
+
+ To remove a key::
+
+ {% query_string foo=None %}
+
+ To use with pagination::
+
+ {% query_string page=page_obj.next_page_number %}
+
+ A custom ``QueryDict`` can also be used::
+
+ {% query_string my_query_dict foo=3 %}
+ """
+ if query_dict is None:
+ query_dict = context.request.GET
+ query_dict = query_dict.copy()
+ for key, value in kwargs.items():
+ if value is None:
+ if key in query_dict:
+ del query_dict[key]
+ elif is_iterable(value) and not isinstance(value, str):
+ query_dict.setlist(key, value)
+ else:
+ query_dict[key] = value
+ if not query_dict:
+ return ""
+ query_string = query_dict.urlencode()
+ return f"?{query_string}"
+
+
@register.tag
def regroup(parser, token):
"""
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 038a2093c4..6af68aebca 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -953,6 +953,78 @@ output (as a string) inside a variable. This is useful if you want to use
{% now "Y" as current_year %}
{% blocktranslate %}Copyright {{ current_year }}{% endblocktranslate %}
+.. templatetag:: query_string
+
+``query_string``
+----------------
+
+.. versionadded:: 5.1
+
+Outputs the query string from a given :class:`~django.http.QueryDict` instance,
+if provided, or ``request.GET`` if not and the
+``django.template.context_processors.request`` context processor is enabled.
+If the ``QueryDict`` is empty, then the output will be an empty string.
+Otherwise, the query string will be returned with a leading ``"?"``.
+
+If not using the ``django.template.context_processors.request`` context
+processor, you must pass either the ``request`` into the template context or a
+``QueryDict`` instance into this tag.
+
+The following example outputs the current query string verbatim. So if the
+query string is ``?color=green&size=M``, the output would be
+``?color=green&size=M``:
+
+.. code-block:: html+django
+
+ {% query_string %}
+
+You can also pass in a custom ``QueryDict`` that will be used instead of
+``request.GET``:
+
+.. code-block:: html+django
+
+ {% query_string my_query_dict %}
+
+Each keyword argument will be added to the query string, replacing any existing
+value for that key. With the query string ``?color=blue``, the following would
+result in ``?color=red&size=S``:
+
+.. code-block:: html+django
+
+ {% query_string color="red" size="S" %}
+
+It is possible to remove parameters by passing ``None`` as a value. With the
+query string ``?color=blue&size=M``, the following would result in ``?size=M``:
+
+.. code-block:: html+django
+
+ {% query_string color=None %}
+
+If the given parameter is a list, the value will remain as a list. For example,
+if ``my_list`` is set to ``["red", "blue"]``, the following would result in
+``?color=red&color=blue``:
+
+.. code-block:: html+django
+
+ {% query_string color=my_list %}
+
+A common example of using this tag is to preserve the current query string when
+displaying a page of results, while adding a link to the next and previous
+pages of results. For example, if the paginator is currently on page 3, and
+the current query string is ``?color=blue&size=M&page=3``, the following code
+would output ``?color=blue&size=M&page=4``:
+
+.. code-block:: html+django
+
+ {% query_string page=page.next_page_number %}
+
+You can also store the value in a variable, for example, if you need multiple
+links to the same page with syntax such as:
+
+.. code-block:: html+django
+
+ {% query_string page=page.next_page_number as next_page %}
+
.. templatetag:: regroup
``regroup``
diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt
index 799a30811f..e352b9c04f 100644
--- a/docs/releases/5.1.txt
+++ b/docs/releases/5.1.txt
@@ -198,6 +198,11 @@ Templates
be made available on the ``Template`` instance. Such data may be used, for
example, by the template loader, or other template clients.
+* The new :ttag:`{% query_string %} <query_string>` template tag allows
+ changing a :class:`~django.http.QueryDict` instance for use in links, for
+ example, to generate a link to the next page while keeping any filtering
+ options in place.
+
Tests
~~~~~
diff --git a/tests/template_tests/syntax_tests/test_query_string.py b/tests/template_tests/syntax_tests/test_query_string.py
new file mode 100644
index 0000000000..13c0dc1d08
--- /dev/null
+++ b/tests/template_tests/syntax_tests/test_query_string.py
@@ -0,0 +1,100 @@
+from django.http import QueryDict
+from django.template import RequestContext
+from django.test import RequestFactory, SimpleTestCase
+
+from ..utils import setup
+
+
+class QueryStringTagTests(SimpleTestCase):
+ def setUp(self):
+ self.request_factory = RequestFactory()
+
+ @setup({"query_string_empty": "{% query_string %}"})
+ def test_query_string_empty(self):
+ request = self.request_factory.get("/")
+ template = self.engine.get_template("query_string_empty")
+ context = RequestContext(request)
+ output = template.render(context)
+ self.assertEqual(output, "")
+
+ @setup({"query_string_non_empty": "{% query_string %}"})
+ def test_query_string_non_empty(self):
+ request = self.request_factory.get("/", {"a": "b"})
+ template = self.engine.get_template("query_string_non_empty")
+ context = RequestContext(request)
+ output = template.render(context)
+ self.assertEqual(output, "?a=b")
+
+ @setup({"query_string_multiple": "{% query_string %}"})
+ def test_query_string_multiple(self):
+ request = self.request_factory.get("/", {"x": "y", "a": "b"})
+ template = self.engine.get_template("query_string_multiple")
+ context = RequestContext(request)
+ output = template.render(context)
+ self.assertEqual(output, "?x=y&amp;a=b")
+
+ @setup({"query_string_replace": "{% query_string a=1 %}"})
+ def test_query_string_replace(self):
+ request = self.request_factory.get("/", {"x": "y", "a": "b"})
+ template = self.engine.get_template("query_string_replace")
+ context = RequestContext(request)
+ output = template.render(context)
+ self.assertEqual(output, "?x=y&amp;a=1")
+
+ @setup({"query_string_add": "{% query_string test_new='something' %}"})
+ def test_query_string_add(self):
+ request = self.request_factory.get("/", {"a": "b"})
+ template = self.engine.get_template("query_string_add")
+ context = RequestContext(request)
+ output = template.render(context)
+ self.assertEqual(output, "?a=b&amp;test_new=something")
+
+ @setup({"query_string_remove": "{% query_string test=None a=1 %}"})
+ def test_query_string_remove(self):
+ request = self.request_factory.get("/", {"test": "value", "a": "1"})
+ template = self.engine.get_template("query_string_remove")
+ context = RequestContext(request)
+ output = template.render(context)
+ self.assertEqual(output, "?a=1")
+
+ @setup(
+ {"query_string_remove_nonexistent": "{% query_string nonexistent=None a=1 %}"}
+ )
+ def test_query_string_remove_nonexistent(self):
+ request = self.request_factory.get("/", {"x": "y", "a": "1"})
+ template = self.engine.get_template("query_string_remove_nonexistent")
+ context = RequestContext(request)
+ output = template.render(context)
+ self.assertEqual(output, "?x=y&amp;a=1")
+
+ @setup({"query_string_list": "{% query_string a=my_list %}"})
+ def test_query_string_add_list(self):
+ request = self.request_factory.get("/")
+ template = self.engine.get_template("query_string_list")
+ context = RequestContext(request, {"my_list": [2, 3]})
+ output = template.render(context)
+ self.assertEqual(output, "?a=2&amp;a=3")
+
+ @setup({"query_string_query_dict": "{% query_string request.GET a=2 %}"})
+ def test_query_string_with_explicit_query_dict(self):
+ request = self.request_factory.get("/", {"a": 1})
+ output = self.engine.render_to_string(
+ "query_string_query_dict", {"request": request}
+ )
+ self.assertEqual(output, "?a=2")
+
+ @setup(
+ {"query_string_query_dict_no_request": "{% query_string my_query_dict a=2 %}"}
+ )
+ def test_query_string_with_explicit_query_dict_and_no_request(self):
+ context = {"my_query_dict": QueryDict("a=1&b=2")}
+ output = self.engine.render_to_string(
+ "query_string_query_dict_no_request", context
+ )
+ self.assertEqual(output, "?a=2&amp;b=2")
+
+ @setup({"query_string_no_request_no_query_dict": "{% query_string %}"})
+ def test_query_string_without_request_or_explicit_query_dict(self):
+ msg = "'Context' object has no attribute 'request'"
+ with self.assertRaisesMessage(AttributeError, msg):
+ self.engine.render_to_string("query_string_no_request_no_query_dict")