diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index a2533198b3..b634b56685 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -351,6 +351,9 @@ class RegexURLResolver(LocaleRegexProvider): return self._resolve_special('500') def reverse(self, lookup_view, *args, **kwargs): + return self._reverse_with_prefix(lookup_view, '', *args, **kwargs) + + def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs): if args and kwargs: raise ValueError("Don't mix *args and **kwargs in call to reverse()!") try: @@ -358,15 +361,16 @@ class RegexURLResolver(LocaleRegexProvider): except (ImportError, AttributeError), e: raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e)) possibilities = self.reverse_dict.getlist(lookup_view) + prefix_norm, prefix_args = normalize(_prefix)[0] for possibility, pattern, defaults in possibilities: for result, params in possibility: if args: - if len(args) != len(params): + if len(args) != len(params) + len(prefix_args): continue unicode_args = [force_unicode(val) for val in args] - candidate = result % dict(zip(params, unicode_args)) + candidate = (prefix_norm + result) % dict(zip(prefix_args + params, unicode_args)) else: - if set(kwargs.keys() + defaults.keys()) != set(params + defaults.keys()): + if set(kwargs.keys() + defaults.keys()) != set(params + defaults.keys() + prefix_args): continue matches = True for k, v in defaults.items(): @@ -376,8 +380,8 @@ class RegexURLResolver(LocaleRegexProvider): if not matches: continue unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()]) - candidate = result % unicode_kwargs - if re.search(u'^%s' % pattern, candidate, re.UNICODE): + candidate = (prefix_norm + result) % unicode_kwargs + if re.search(u'^%s%s' % (_prefix, pattern), candidate, re.UNICODE): return candidate # lookup_view can be URL label, or dotted path, or callable, Any of # these can be passed in at the top, but callables are not friendly in @@ -469,8 +473,7 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current if ns_pattern: resolver = get_ns_resolver(ns_pattern, resolver) - return iri_to_uri(u'%s%s' % - (prefix, resolver.reverse(view, *args, **kwargs))) + return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs)) reverse_lazy = lazy(reverse, str) diff --git a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py index 6f6e8893e7..fa892a4346 100644 --- a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py +++ b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py @@ -44,12 +44,13 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views', (r'^default/', include(default_testobj.urls)), (r'^other1/', include(otherobj1.urls)), - (r'^other2/', include(otherobj2.urls)), + (r'^other[246]/', include(otherobj2.urls)), - (r'^ns-included1/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')), + (r'^ns-included[135]/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')), (r'^ns-included2/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns2')), (r'^included/', include('regressiontests.urlpatterns_reverse.included_namespace_urls')), + (r'^inc(?P\d+)/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns5')), (r'^ns-outer/(?P\d+)/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-outer')), diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 307d8daabb..a5df26f1a1 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -48,6 +48,10 @@ resolve_test_data = ( # Nested namespaces ('/ns-included1/test3/inner/42/37/', 'urlobject-view', 'testapp', 'inc-ns1:test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}), ('/ns-included1/ns-included4/ns-included2/test3/inner/42/37/', 'urlobject-view', 'testapp', 'inc-ns1:inc-ns4:inc-ns2:test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}), + + # Namespaces capturing variables + ('/inc70/', 'inner-nothing', None, 'inc-ns5', views.empty_view, tuple(), {'outer': '70'}), + ('/inc78/extra/foobar/', 'inner-extra', None, 'inc-ns5', views.empty_view, tuple(), {'outer':'78', 'extra':'foobar'}), ) test_data = ( @@ -379,6 +383,13 @@ class NamespaceTests(TestCase): self.assertEqual('/+%5C$*/included/normal/42/37/', reverse('special:inc-normal-view', kwargs={'arg1':42, 'arg2':37})) self.assertEqual('/+%5C$*/included/+%5C$*/', reverse('special:inc-special-view')) + def test_namespaces_with_variables(self): + "Namespace prefixes can capture variables: see #15900" + self.assertEqual('/inc70/', reverse('inc-ns5:inner-nothing', kwargs={'outer': '70'})) + self.assertEqual('/inc78/extra/foobar/', reverse('inc-ns5:inner-extra', kwargs={'outer':'78', 'extra':'foobar'})) + self.assertEqual('/inc70/', reverse('inc-ns5:inner-nothing', args=['70'])) + self.assertEqual('/inc78/extra/foobar/', reverse('inc-ns5:inner-extra', args=['78','foobar'])) + class RequestURLconfTests(TestCase): def setUp(self): self.root_urlconf = settings.ROOT_URLCONF