1
0
mirror of https://github.com/django/django.git synced 2024-12-22 09:05:43 +00:00

Fixed #25582 -- Added support for query and fragment to django.urls.reverse().

This commit is contained in:
Ben Cardy 2024-12-11 19:40:28 +00:00 committed by GitHub
parent 2ce4545de1
commit f30b527f17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 136 additions and 6 deletions

View File

@ -146,6 +146,7 @@ answer newbie questions, and generally made Django that much better:
Batman Batman
Batuhan Taskaya <batuhanosmantaskaya@gmail.com> Batuhan Taskaya <batuhanosmantaskaya@gmail.com>
Baurzhan Ismagulov <ibr@radix50.net> Baurzhan Ismagulov <ibr@radix50.net>
Ben Cardy <me@bencardy.co.uk>
Ben Dean Kawamura <ben.dean.kawamura@gmail.com> Ben Dean Kawamura <ben.dean.kawamura@gmail.com>
Ben Firshman <ben@firshman.co.uk> Ben Firshman <ben@firshman.co.uk>
Ben Godfrey <http://aftnn.org> Ben Godfrey <http://aftnn.org>

View File

@ -1,7 +1,8 @@
from urllib.parse import unquote, urlsplit, urlunsplit from urllib.parse import unquote, urlencode, urlsplit, urlunsplit
from asgiref.local import Local from asgiref.local import Local
from django.http import QueryDict
from django.utils.functional import lazy from django.utils.functional import lazy
from django.utils.translation import override from django.utils.translation import override
@ -24,7 +25,16 @@ def resolve(path, urlconf=None):
return get_resolver(urlconf).resolve(path) return get_resolver(urlconf).resolve(path)
def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None): def reverse(
viewname,
urlconf=None,
args=None,
kwargs=None,
current_app=None,
*,
query=None,
fragment=None,
):
if urlconf is None: if urlconf is None:
urlconf = get_urlconf() urlconf = get_urlconf()
resolver = get_resolver(urlconf) resolver = get_resolver(urlconf)
@ -85,7 +95,17 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None):
ns_pattern, resolver, tuple(ns_converters.items()) ns_pattern, resolver, tuple(ns_converters.items())
) )
return resolver._reverse_with_prefix(view, prefix, *args, **kwargs) resolved_url = resolver._reverse_with_prefix(view, prefix, *args, **kwargs)
if query is not None:
if isinstance(query, QueryDict):
query_string = query.urlencode()
else:
query_string = urlencode(query, doseq=True)
if query_string:
resolved_url += "?" + query_string
if fragment is not None:
resolved_url += "#" + fragment
return resolved_url
reverse_lazy = lazy(reverse, str) reverse_lazy = lazy(reverse, str)

View File

@ -10,7 +10,7 @@
The ``reverse()`` function can be used to return an absolute path reference The ``reverse()`` function can be used to return an absolute path reference
for a given view and optional parameters, similar to the :ttag:`url` tag: for a given view and optional parameters, similar to the :ttag:`url` tag:
.. function:: reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None) .. function:: reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None, *, query=None, fragment=None)
``viewname`` can be a :ref:`URL pattern name <naming-url-patterns>` or the ``viewname`` can be a :ref:`URL pattern name <naming-url-patterns>` or the
callable view object used in the URLconf. For example, given the following callable view object used in the URLconf. For example, given the following
@ -67,6 +67,33 @@ namespaces into URLs on specific application instances, according to the
The ``urlconf`` argument is the URLconf module containing the URL patterns to The ``urlconf`` argument is the URLconf module containing the URL patterns to
use for reversing. By default, the root URLconf for the current thread is used. use for reversing. By default, the root URLconf for the current thread is used.
The ``query`` keyword argument specifies parameters to be added to the returned
URL. It can accept an instance of :class:`~django.http.QueryDict` (such as
``request.GET``) or any value compatible with :func:`urllib.parse.urlencode`.
The encoded query string is appended to the resolved URL, prefixed by a ``?``.
The ``fragment`` keyword argument specifies a fragment identifier to be
appended to the returned URL (that is, after the path and query string,
preceded by a ``#``).
For example:
.. code-block:: pycon
>>> from django.urls import reverse
>>> reverse("admin:index", query={"q": "biscuits", "page": 2}, fragment="results")
'/admin/?q=biscuits&page=2#results'
>>> reverse("admin:index", query=[("color", "blue"), ("color", 1), ("none", None)])
'/admin/?color=blue&color=1&none=None'
>>> reverse("admin:index", query={"has empty spaces": "also has empty spaces!"})
'/admin/?has+empty+spaces=also+has+empty+spaces%21'
>>> reverse("admin:index", fragment="no encoding is done")
'/admin/#no encoding is done'
.. versionchanged:: 5.2
The ``query`` and ``fragment`` arguments were added.
.. note:: .. note::
The string returned by ``reverse()`` is already The string returned by ``reverse()`` is already

View File

@ -370,7 +370,9 @@ Tests
URLs URLs
~~~~ ~~~~
* ... * :func:`~django.urls.reverse` now accepts ``query`` and ``fragment`` keyword
arguments, allowing the addition of a query string and/or fragment identifier
in the generated URL, respectively.
Utilities Utilities
~~~~~~~~~ ~~~~~~~~~

View File

@ -11,7 +11,12 @@ from admin_scripts.tests import AdminScriptTestCase
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.http import HttpRequest, HttpResponsePermanentRedirect, HttpResponseRedirect from django.http import (
HttpRequest,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
QueryDict,
)
from django.shortcuts import redirect from django.shortcuts import redirect
from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings
from django.test.utils import override_script_prefix from django.test.utils import override_script_prefix
@ -531,6 +536,81 @@ class URLPatternReverse(SimpleTestCase):
with self.assertRaises(NoReverseMatch): with self.assertRaises(NoReverseMatch):
reverse(views.view_func_from_cbv) reverse(views.view_func_from_cbv)
def test_reverse_with_query(self):
self.assertEqual(
reverse("test", query={"hello": "world", "foo": 123}),
"/test/1?hello=world&foo=123",
)
def test_reverse_with_query_sequences(self):
cases = [
[("hello", "world"), ("foo", 123), ("foo", 456)],
(("hello", "world"), ("foo", 123), ("foo", 456)),
{"hello": "world", "foo": (123, 456)},
]
for query in cases:
with self.subTest(query=query):
self.assertEqual(
reverse("test", query=query), "/test/1?hello=world&foo=123&foo=456"
)
def test_reverse_with_fragment(self):
self.assertEqual(reverse("test", fragment="tab-1"), "/test/1#tab-1")
def test_reverse_with_fragment_not_encoded(self):
self.assertEqual(
reverse("test", fragment="tab 1 is the best!"), "/test/1#tab 1 is the best!"
)
def test_reverse_with_query_and_fragment(self):
self.assertEqual(
reverse("test", query={"hello": "world", "foo": 123}, fragment="tab-1"),
"/test/1?hello=world&foo=123#tab-1",
)
def test_reverse_with_empty_fragment(self):
self.assertEqual(reverse("test", fragment=None), "/test/1")
self.assertEqual(reverse("test", fragment=""), "/test/1#")
def test_reverse_with_invalid_fragment(self):
cases = [0, False, {}, [], set(), ()]
for fragment in cases:
with self.subTest(fragment=fragment):
with self.assertRaises(TypeError):
reverse("test", fragment=fragment)
def test_reverse_with_empty_query(self):
cases = [None, "", {}, [], set(), (), QueryDict()]
for query in cases:
with self.subTest(query=query):
self.assertEqual(reverse("test", query=query), "/test/1")
def test_reverse_with_invalid_query(self):
cases = [0, False, [1, 3, 5], {1, 2, 3}]
for query in cases:
with self.subTest(query=query):
with self.assertRaises(TypeError):
print(reverse("test", query=query))
def test_reverse_encodes_query_string(self):
self.assertEqual(
reverse(
"test",
query={
"hello world": "django project",
"foo": [123, 456],
"@invalid": ["?", "!", "a b"],
},
),
"/test/1?hello+world=django+project&foo=123&foo=456"
"&%40invalid=%3F&%40invalid=%21&%40invalid=a+b",
)
def test_reverse_with_query_from_querydict(self):
query_string = "a=1&b=2&b=3&c=4"
query_dict = QueryDict(query_string)
self.assertEqual(reverse("test", query=query_dict), f"/test/1?{query_string}")
class ResolverTests(SimpleTestCase): class ResolverTests(SimpleTestCase):
def test_resolver_repr(self): def test_resolver_repr(self):