diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index bad5490dcb75..6c681f9d8d32 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -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): diff --git a/docs/releases/6.0.1.txt b/docs/releases/6.0.1.txt index 90158b6ff2aa..753d2c87ad0e 100644 --- a/docs/releases/6.0.1.txt +++ b/docs/releases/6.0.1.txt @@ -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`). diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt index 300cf2041c77..b5ab1888cb63 100644 --- a/docs/topics/http/shortcuts.txt +++ b/docs/topics/http/shortcuts.txt @@ -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()`` ======================= diff --git a/tests/resolve_url/tests.py b/tests/resolve_url/tests.py index 26fe783bf36a..e5cdfe9618ee 100644 --- a/tests/resolve_url/tests.py +++ b/tests/resolve_url/tests.py @@ -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") @@ -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 diff --git a/tests/resolve_url/urls.py b/tests/resolve_url/urls.py index 4cd4ffa03fd1..e0ce8b5149ad 100644 --- a/tests/resolve_url/urls.py +++ b/tests/resolve_url/urls.py @@ -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//", params_view, name="params-view"), + path("params-cbv//", params_cbv, name="params-cbv"), path("some-url/", some_view, name="some-view"), + path("some-cbv/", some_cbv, name="some-cbv"), ] diff --git a/tests/resolve_url/views.py b/tests/resolve_url/views.py new file mode 100644 index 000000000000..987c746d6724 --- /dev/null +++ b/tests/resolve_url/views.py @@ -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() diff --git a/tests/urlpatterns/lazy_path_urls.py b/tests/urlpatterns/lazy_path_urls.py new file mode 100644 index 000000000000..01fcfcb3ae05 --- /dev/null +++ b/tests/urlpatterns/lazy_path_urls.py @@ -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//"), views.empty_view, name="lazy"), +] diff --git a/tests/urlpatterns/test_resolvers.py b/tests/urlpatterns/test_resolvers.py index 99b2f9154c0b..f61456a92190 100644 --- a/tests/urlpatterns/test_resolvers.py +++ b/tests/urlpatterns/test_resolvers.py @@ -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): @@ -19,6 +21,21 @@ def test_has_converters(self): self.assertEqual(len(RoutePattern("translated/").converters), 1) self.assertEqual(len(RoutePattern(_("translated/")).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//")) + result = pattern.match("test/123/child/") + self.assertEqual(result, ("child/", (), {"pk": 123})) + class ResolverCacheTests(SimpleTestCase): @override_settings(ROOT_URLCONF="urlpatterns.path_urls") @@ -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)