diff --git a/AUTHORS b/AUTHORS index 34e729bb1f..9be6412d19 100644 --- a/AUTHORS +++ b/AUTHORS @@ -773,6 +773,7 @@ answer newbie questions, and generally made Django that much better: Stephan Jaekel Stephen Burrows Steven L. Smith (fvox13) + Steven Noorbergen (Xaroth) Stuart Langridge Sujay S Kumar Sune Kirkeby diff --git a/django/urls/base.py b/django/urls/base.py index 6dccdd2e7d..1e11e05bec 100644 --- a/django/urls/base.py +++ b/django/urls/base.py @@ -49,6 +49,7 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None): resolved_path = [] ns_pattern = '' + ns_converters = {} while path: ns = path.pop() current_ns = current_path.pop() if current_path else None @@ -74,6 +75,7 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None): extra, resolver = resolver.namespace_dict[ns] resolved_path.append(ns) ns_pattern = ns_pattern + extra + ns_converters.update(resolver.pattern.converters) except KeyError as key: if resolved_path: raise NoReverseMatch( @@ -83,7 +85,7 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None): else: raise NoReverseMatch("%s is not a registered namespace" % key) if ns_pattern: - resolver = get_ns_resolver(ns_pattern, resolver) + resolver = get_ns_resolver(ns_pattern, resolver, tuple(ns_converters.items())) return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs)) diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index 7343f6616a..ce8c7ffa32 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -68,11 +68,13 @@ def get_resolver(urlconf=None): @functools.lru_cache(maxsize=None) -def get_ns_resolver(ns_pattern, resolver): +def get_ns_resolver(ns_pattern, resolver, converters): # Build a namespaced resolver for the given parent URLconf pattern. # This makes it possible to have captured parameters in the parent # URLconf pattern. - ns_resolver = URLResolver(RegexPattern(ns_pattern), resolver.url_patterns) + pattern = RegexPattern(ns_pattern) + pattern.converters = dict(converters) + ns_resolver = URLResolver(pattern, resolver.url_patterns) return URLResolver(RegexPattern(r'^/'), [ns_resolver]) @@ -439,7 +441,7 @@ class URLResolver: new_matches, p_pattern + pat, {**defaults, **url_pattern.default_kwargs}, - {**self.pattern.converters, **converters} + {**self.pattern.converters, **url_pattern.pattern.converters, **converters} ) ) for namespace, (prefix, sub_pattern) in url_pattern.namespace_dict.items(): diff --git a/docs/releases/2.0.6.txt b/docs/releases/2.0.6.txt index 5d401a7ea9..1c9d0982fa 100644 --- a/docs/releases/2.0.6.txt +++ b/docs/releases/2.0.6.txt @@ -11,3 +11,6 @@ Bugfixes * Fixed a regression that broke custom template filters that use decorators (:ticket:`29400`). + +* Fixed detection of custom URL converters in included patterns + (:ticket:`29415`). diff --git a/tests/urlpatterns/path_base64_urls.py b/tests/urlpatterns/path_base64_urls.py index 872636f06c..9b69f929fe 100644 --- a/tests/urlpatterns/path_base64_urls.py +++ b/tests/urlpatterns/path_base64_urls.py @@ -1,9 +1,15 @@ -from django.urls import path, register_converter +from django.urls import include, path, register_converter from . import converters, views register_converter(converters.Base64Converter, 'base64') +subpatterns = [ + path('/', views.empty_view, name='subpattern-base64'), +] + urlpatterns = [ path('base64//', views.empty_view, name='base64'), + path('base64//subpatterns/', include(subpatterns)), + path('base64//namespaced/', include((subpatterns, 'namespaced-base64'))), ] diff --git a/tests/urlpatterns/tests.py b/tests/urlpatterns/tests.py index b200aed06d..b3d97ec5b9 100644 --- a/tests/urlpatterns/tests.py +++ b/tests/urlpatterns/tests.py @@ -8,6 +8,15 @@ from django.urls import Resolver404, path, resolve, reverse from .converters import DynamicConverter from .views import empty_view +included_kwargs = {'base': b'hello', 'value': b'world'} +converter_test_data = ( + # ('url', ('url_name', 'app_name', {kwargs})), + # aGVsbG8= is 'hello' encoded in base64. + ('/base64/aGVsbG8=/', ('base64', '', {'value': b'hello'})), + ('/base64/aGVsbG8=/subpatterns/d29ybGQ=/', ('subpattern-base64', '', included_kwargs)), + ('/base64/aGVsbG8=/namespaced/d29ybGQ=/', ('subpattern-base64', 'namespaced-base64', included_kwargs)), +) + @override_settings(ROOT_URLCONF='urlpatterns.path_urls') class SimplifiedURLTests(SimpleTestCase): @@ -44,15 +53,22 @@ class SimplifiedURLTests(SimpleTestCase): self.assertEqual(url, '/articles/2015/4/12/') @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls') - def test_non_identical_converter_resolve(self): - match = resolve('/base64/aGVsbG8=/') # base64 of 'hello' - self.assertEqual(match.url_name, 'base64') - self.assertEqual(match.kwargs, {'value': b'hello'}) + def test_converter_resolve(self): + for url, (url_name, app_name, kwargs) in converter_test_data: + with self.subTest(url=url): + match = resolve(url) + self.assertEqual(match.url_name, url_name) + self.assertEqual(match.app_name, app_name) + self.assertEqual(match.kwargs, kwargs) @override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls') - def test_non_identical_converter_reverse(self): - url = reverse('base64', kwargs={'value': b'hello'}) - self.assertEqual(url, '/base64/aGVsbG8=/') + def test_converter_reverse(self): + for expected, (url_name, app_name, kwargs) in converter_test_data: + if app_name: + url_name = '%s:%s' % (app_name, url_name) + with self.subTest(url=url_name): + url = reverse(url_name, kwargs=kwargs) + self.assertEqual(url, expected) def test_path_inclusion_is_matchable(self): match = resolve('/included_urls/extra/something/')