Skip to content

Commit 02dcbe3

Browse files
committed
Fixed #11559 -- Fixed the URL resolver to be able to handle captured parameters in parent URLconfs when also using namespaces. Thanks, cwb, ungenio and hvdklauw.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@16608 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 351d5da commit 02dcbe3

File tree

3 files changed

+43
-14
lines changed

3 files changed

+43
-14
lines changed

django/core/urlresolvers.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323

2424
_resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
25+
_ns_resolver_cache = {} # Maps namespaces to RegexURLResolver instances.
2526
_callable_cache = {} # Maps view and url pattern names to their view functions.
2627

2728
# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
@@ -91,20 +92,20 @@ def get_callable(lookup_view, can_fail=False):
9192
lookup_view = getattr(import_module(mod_name), func_name)
9293
if not callable(lookup_view):
9394
raise ViewDoesNotExist(
94-
"Could not import %s.%s. View is not callable."
95-
% (mod_name, func_name))
95+
"Could not import %s.%s. View is not callable." %
96+
(mod_name, func_name))
9697
except AttributeError:
9798
if not can_fail:
9899
raise ViewDoesNotExist(
99-
"Could not import %s. View does not exist in module %s."
100-
% (lookup_view, mod_name))
100+
"Could not import %s. View does not exist in module %s." %
101+
(lookup_view, mod_name))
101102
except ImportError:
102103
parentmod, submod = get_mod_func(mod_name)
103104
if (not can_fail and submod != '' and
104105
not module_has_submodule(import_module(parentmod), submod)):
105106
raise ViewDoesNotExist(
106-
"Could not import %s. Parent module %s does not exist."
107-
% (lookup_view, mod_name))
107+
"Could not import %s. Parent module %s does not exist." %
108+
(lookup_view, mod_name))
108109
if not can_fail:
109110
raise
110111
return lookup_view
@@ -117,6 +118,15 @@ def get_resolver(urlconf):
117118
return RegexURLResolver(r'^/', urlconf)
118119
get_resolver = memoize(get_resolver, _resolver_cache, 1)
119120

121+
def get_ns_resolver(ns_pattern, resolver):
122+
# Build a namespaced resolver for the given parent urlconf pattern.
123+
# This makes it possible to have captured parameters in the parent
124+
# urlconf pattern.
125+
ns_resolver = RegexURLResolver(ns_pattern,
126+
resolver.url_patterns)
127+
return RegexURLResolver(r'^/', [ns_resolver])
128+
get_ns_resolver = memoize(get_ns_resolver, _ns_resolver_cache, 2)
129+
120130
def get_mod_func(callback):
121131
# Converts 'django.views.news.stories.story_detail' to
122132
# ['django.views.news.stories', 'story_detail']
@@ -424,6 +434,7 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current
424434
path = parts[1:]
425435

426436
resolved_path = []
437+
ns_pattern = ''
427438
while path:
428439
ns = path.pop()
429440

@@ -432,34 +443,43 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current
432443
app_list = resolver.app_dict[ns]
433444
# Yes! Path part matches an app in the current Resolver
434445
if current_app and current_app in app_list:
435-
# If we are reversing for a particular app, use that namespace
446+
# If we are reversing for a particular app,
447+
# use that namespace
436448
ns = current_app
437449
elif ns not in app_list:
438-
# The name isn't shared by one of the instances (i.e., the default)
439-
# so just pick the first instance as the default.
450+
# The name isn't shared by one of the instances
451+
# (i.e., the default) so just pick the first instance
452+
# as the default.
440453
ns = app_list[0]
441454
except KeyError:
442455
pass
443456

444457
try:
445458
extra, resolver = resolver.namespace_dict[ns]
446459
resolved_path.append(ns)
447-
prefix = prefix + extra
460+
ns_pattern = ns_pattern + extra
448461
except KeyError, key:
449462
if resolved_path:
450-
raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path)))
463+
raise NoReverseMatch(
464+
"%s is not a registered namespace inside '%s'" %
465+
(key, ':'.join(resolved_path)))
451466
else:
452-
raise NoReverseMatch("%s is not a registered namespace" % key)
467+
raise NoReverseMatch("%s is not a registered namespace" %
468+
key)
469+
if ns_pattern:
470+
resolver = get_ns_resolver(ns_pattern, resolver)
453471

454-
return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view,
455-
*args, **kwargs)))
472+
return iri_to_uri(u'%s%s' %
473+
(prefix, resolver.reverse(view, *args, **kwargs)))
456474

457475
reverse_lazy = lazy(reverse, str)
458476

459477
def clear_url_caches():
460478
global _resolver_cache
479+
global _ns_resolver_cache
461480
global _callable_cache
462481
_resolver_cache.clear()
482+
_ns_resolver_cache.clear()
463483
_callable_cache.clear()
464484

465485
def set_script_prefix(prefix):

tests/regressiontests/urlpatterns_reverse/namespace_urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ def urls(self):
4444

4545
(r'^included/', include('regressiontests.urlpatterns_reverse.included_namespace_urls')),
4646

47+
(r'^ns-outer/(?P<outer>\d+)/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-outer')),
48+
4749
)

tests/regressiontests/urlpatterns_reverse/tests.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,13 @@ def test_namespace_pattern(self):
327327
self.assertEqual('/ns-included1/normal/37/42/', reverse('inc-ns1:inc-normal-view', args=[37,42]))
328328
self.assertEqual('/ns-included1/normal/42/37/', reverse('inc-ns1:inc-normal-view', kwargs={'arg1':42, 'arg2':37}))
329329

330+
def test_namespace_pattern_with_variable_prefix(self):
331+
"When using a include with namespaces when there is a regex variable in front of it"
332+
self.assertEqual('/ns-outer/42/normal/', reverse('inc-outer:inc-normal-view', kwargs={'outer':42}))
333+
self.assertEqual('/ns-outer/42/normal/', reverse('inc-outer:inc-normal-view', args=[42]))
334+
self.assertEqual('/ns-outer/42/normal/37/4/', reverse('inc-outer:inc-normal-view', kwargs={'outer':42, 'arg1': 37, 'arg2': 4}))
335+
self.assertEqual('/ns-outer/42/normal/37/4/', reverse('inc-outer:inc-normal-view', args=[42, 37, 4]))
336+
330337
def test_multiple_namespace_pattern(self):
331338
"Namespaces can be embedded"
332339
self.assertEqual('/ns-included1/test3/inner/', reverse('inc-ns1:test-ns3:urlobject-view'))

0 commit comments

Comments
 (0)