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:
parent
2ce4545de1
commit
f30b527f17
1
AUTHORS
1
AUTHORS
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user