Commits

Anonymous committed ef0a828

Fixed #15900 -- Calls to reverse with nested namespaced urls are escaped properly and capture parameters as expected.

Thanks to teolicy for the report, and dmclain for the patch.

Comments (0)

Files changed (3)

django/core/urlresolvers.py

         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:
         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():
                     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
         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)
 

tests/regressiontests/urlpatterns_reverse/namespace_urls.py

     (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<outer>\d+)/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns5')),
 
     (r'^ns-outer/(?P<outer>\d+)/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-outer')),
 

tests/regressiontests/urlpatterns_reverse/tests.py

     # 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 = (
         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