Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions django/urls/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ def match(self, path):
if self._route == path:
return "", (), {}
# If this isn't an endpoint, the path should start with the route.
elif path.startswith(self._route):
return path.removeprefix(self._route), (), {}
elif path.startswith(route := str(self._route)):
return path.removeprefix(route), (), {}
return None

def check(self):
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/6.0.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ Bugfixes
* Fixed a crash in Django 6.0 caused by infinite recursion when calling
``repr()`` on an unevaluated ``django.utils.csp.LazyNonce`` instance
(:ticket:`36810`).

* Fixed a regression in Django 6.0 where :func:`~django.urls.path` routes
defined using :func:`~django.utils.translation.gettext_lazy` failed to
resolve correctly (:ticket:`36796`).
72 changes: 72 additions & 0 deletions docs/topics/http/shortcuts.txt
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,78 @@ original HTTP method::
return redirect(obj, preserve_request=True)
# ...

``resolve_url()``
=================

.. function:: resolve_url(to, *args, **kwargs)

Returns a URL string by resolving and normalizing the given ``to`` argument
into a concrete URL. The parameter ``to`` may be:

* An object implementing :meth:`~django.db.models.Model.get_absolute_url`,
in which case the method will be called and its result returned.

* A view name, view function, or view class, possibly with arguments passed
as ``*args`` and ``**kwargs``, in which case :func:`~django.urls.reverse`
will be used to reverse-resolve the view.

* A URL string, which will be returned unchanged.

This function is used internally by the :func:`redirect` shortcut to
determine the target URL for the redirect location.

Examples
--------

#. Resolving a URL for a model that defines
:meth:`~django.db.models.Model.get_absolute_url`:

.. code-block:: python
:caption: ``models.py``

from django.db import models
from django.urls import reverse


class Article(models.Model):
title = models.CharField(max_length=100)

def get_absolute_url(self):
return reverse("article-detail", args=[self.pk])


.. code-block:: python
:caption: ``views.py``

from django.http import JsonResponse
from django.shortcuts import get_object_or_404, resolve_url
from .models import Article


def article_api_view(request, pk):
"""Return metadata about an article, including its canonical URL."""
article = get_object_or_404(Article, pk=pk)
return JsonResponse(
{
"id": article.pk,
"title": article.title,
"url": resolve_url(article),
}
)

#. Resolving a target URL for use outside of a redirect, such as in an HTTP
response header::

from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import resolve_url


def login_success(request):
response = HttpResponse("Login successful")
response["X-Next-URL"] = resolve_url(settings.LOGIN_REDIRECT_URL)
return response

``get_object_or_404()``
=======================

Expand Down
24 changes: 23 additions & 1 deletion tests/resolve_url/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.urls import NoReverseMatch, reverse_lazy

from .models import UnimportantThing
from .urls import some_view
from .views import params_cbv, params_view, some_cbv, some_view


@override_settings(ROOT_URLCONF="resolve_url.urls")
Expand Down Expand Up @@ -50,6 +50,28 @@ def test_view_function(self):
resolved_url = resolve_url(some_view)
self.assertEqual("/some-url/", resolved_url)

def test_view_function_with_kwargs(self):
self.assertEqual("/params/django/", resolve_url(params_view, slug="django"))

def test_view_function_with_args(self):
self.assertEqual("/params/django/", resolve_url(params_view, "django"))

def test_class_based_view(self):
self.assertEqual("/some-cbv/", resolve_url(some_cbv))

def test_class_based_view_with_kwargs(self):
self.assertEqual("/params-cbv/5/", resolve_url(params_cbv, pk=5))

def test_class_based_view_with_args(self):
self.assertEqual("/params-cbv/5/", resolve_url(params_cbv, 5))

def test_missing_params_raise_no_reverse_match(self):
with self.assertRaises(NoReverseMatch):
resolve_url(params_view)

with self.assertRaises(NoReverseMatch):
resolve_url(params_cbv)

def test_lazy_reverse(self):
"""
Passing the result of reverse_lazy is resolved to a real URL
Expand Down
8 changes: 4 additions & 4 deletions tests/resolve_url/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.urls import path


def some_view(request):
pass

from .views import params_cbv, params_view, some_cbv, some_view

urlpatterns = [
path("params/<slug:slug>/", params_view, name="params-view"),
path("params-cbv/<int:pk>/", params_cbv, name="params-cbv"),
path("some-url/", some_view, name="some-view"),
path("some-cbv/", some_cbv, name="some-cbv"),
]
24 changes: 24 additions & 0 deletions tests/resolve_url/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.http import HttpResponse
from django.views import View


def some_view(request):
return HttpResponse("ok")


def params_view(request, slug):
return HttpResponse(f"Params: {slug}")


class SomeView(View):
def get(self, request):
return HttpResponse("ok")


class ParamsView(View):
def get(self, request, pk):
return HttpResponse(f"Params: {pk}")


some_cbv = SomeView.as_view()
params_cbv = ParamsView.as_view()
9 changes: 9 additions & 0 deletions tests/urlpatterns/lazy_path_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import include, path
from django.utils.translation import gettext_lazy as _

from . import views

urlpatterns = [
path(_("included_urls/"), include("urlpatterns.included_urls")),
path(_("lazy/<slug:slug>/"), views.empty_view, name="lazy"),
]
31 changes: 31 additions & 0 deletions tests/urlpatterns/test_resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django.urls.resolvers import RegexPattern, RoutePattern, get_resolver
from django.utils.translation import gettext_lazy as _

from . import views


class RegexPatternTests(SimpleTestCase):
def test_str(self):
Expand All @@ -19,6 +21,21 @@ def test_has_converters(self):
self.assertEqual(len(RoutePattern("translated/<int:foo>").converters), 1)
self.assertEqual(len(RoutePattern(_("translated/<int:foo>")).converters), 1)

def test_match_lazy_route_without_converters(self):
pattern = RoutePattern(_("test/"))
result = pattern.match("test/child/")
self.assertEqual(result, ("child/", (), {}))

def test_match_lazy_route_endpoint(self):
pattern = RoutePattern(_("test/"), is_endpoint=True)
result = pattern.match("test/")
self.assertEqual(result, ("", (), {}))

def test_match_lazy_route_with_converters(self):
pattern = RoutePattern(_("test/<int:pk>/"))
result = pattern.match("test/123/child/")
self.assertEqual(result, ("child/", (), {"pk": 123}))


class ResolverCacheTests(SimpleTestCase):
@override_settings(ROOT_URLCONF="urlpatterns.path_urls")
Expand All @@ -27,3 +44,17 @@ def test_resolver_cache_default__root_urlconf(self):
# settings.ROOT_URLCONF is the same cached object.
self.assertIs(get_resolver(), get_resolver("urlpatterns.path_urls"))
self.assertIsNot(get_resolver(), get_resolver("urlpatterns.path_dynamic_urls"))


class ResolverLazyIncludeTests(SimpleTestCase):

def test_lazy_route_resolves(self):
resolver = get_resolver("urlpatterns.lazy_path_urls")
for url_path, name in [
("/lazy/test-me/", "lazy"),
("/included_urls/extra/test/", "inner-extra"),
]:
with self.subTest(name=name):
match = resolver.resolve(url_path)
self.assertEqual(match.func, views.empty_view)
self.assertEqual(match.url_name, name)