diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 5911865..4c9c05b 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -96,7 +96,12 @@ class BaseHandler(object): urlresolvers.set_urlconf(urlconf) resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) - resolver_match = resolver.resolve(request.path_info) + # Use resolved to try to resolve matching URL patterns one by one + tried = [] + resolved = {} + + while response is None: + resolver_match = resolver.resolve(request.path_info, tried=tried, resolved=resolved) callback, callback_args, callback_kwargs = resolver_match request.resolver_match = resolver_match @@ -108,20 +158,25 @@ class BaseHandler(object): if response: break - if response is None: - wrapped_callback = self.make_view_atomic(callback) - try: - response = wrapped_callback(request, *callback_args, **callback_kwargs) - except Exception as e: - # If the view raised an exception, run it through exception - # middleware, and if the exception middleware returns a - # response, use that. Otherwise, reraise the exception. - for middleware_method in self._exception_middleware: - response = middleware_method(request, e) - if response: - break - if response is None: - raise + if response is None: + wrapped_callback = self.make_view_atomic(callback) + try: + response = wrapped_callback(request, *callback_args, **callback_kwargs) + except urlresolvers.DoesNotResolve: + # Continue resolve URLs if the view raises + # urlresolvers.DoesNotResolve exception to indicate + # the url pattern does not match. + continue + except Exception as e: + # If the view raised an exception, run it through exception + # middleware, and if the exception middleware returns a + # response, use that. Otherwise, reraise the exception. + for middleware_method in self._exception_middleware: + response = middleware_method(request, e) + if response: + break + if response is None: + raise # Complain if the view returned None (a common error). if response is None: diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index af3df83..055136e 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -73,6 +73,10 @@ def __repr__(self): class Resolver404(Http404): pass +class DoesNotResolve(Http404): + pass + + class NoReverseMatch(Exception): # Don't make this raise an error when used in a template. silent_variable_failure = True @@ -205,7 +210,11 @@ class RegexURLPattern(LocaleRegexProvider): return self._callback_str = prefix + '.' + self._callback_str - def resolve(self, path): + def resolve(self, path, resolved=None, caller=None): + if resolved is not None: + if caller not in resolved: + resolved[caller] = set() + resolved[caller].add(self) match = self.regex.search(path) if match: # If there are any named groups, use those as kwargs, ignoring @@ -329,14 +339,23 @@ class RegexURLResolver(LocaleRegexProvider): self._populate() return self._app_dict[language_code] - def resolve(self, path): - tried = [] + def resolve(self, path, tried=None, resolved=None, caller=None): + if tried is None: + tried = [] + if resolved is None: + resolved = {} + if caller not in resolved: + resolved[caller] = set() match = self.regex.search(path) if match: + if self not in resolved: + resolved[self] = set() new_path = path[match.end():] for pattern in self.url_patterns: + if pattern in resolved[self]: + continue try: - sub_match = pattern.resolve(new_path) + sub_match = pattern.resolve(new_path, resolved=resolved, caller=self) except Resolver404 as e: sub_tried = e.args[0].get('tried') if sub_tried is not None: @@ -355,7 +372,11 @@ class RegexURLResolver(LocaleRegexProvider): sub_match_dict.update(sub_match.kwargs) return ResolverMatch(sub_match.func, sub_match.args, sub_match_dict, sub_match.url_name, self.app_name or sub_match.app_name, [self.namespace] + sub_match.namespaces) tried.append([pattern]) + resolved[self].add(pattern) + del resolved[self] + resolved[caller].add(self) raise Resolver404({'tried': tried, 'path': new_path}) + resolved[caller].add(self) raise Resolver404({'path': path}) @property --- a/tests/view_tests/tests/test_specials.py +++ b/tests/view_tests/tests/test_specials.py @@ -36,3 +36,33 @@ class URLHandling(TestCase): """ response = self.client.get('/permanent_nonascii_redirect/') self.assertRedirects(response, self.redirect_target, status_code=301) + + def test_overlapping_urls_reverse(self): + from django.core import urlresolvers + url = urlresolvers.reverse('overlapping_view1', kwargs={'title': 'sometitle'}) + self.assertEqual(url, '/overlapping_view/sometitle/') + url = urlresolvers.reverse('overlapping_view2', kwargs={'author': 'someauthor'}) + self.assertEqual(url, '/overlapping_view/someauthor/') + + def test_overlapping_urls_resolve(self): + response = self.client.get('/overlapping_view/sometitle/') + self.assertContains(response, 'overlapping_view2') + + def test_overlapping_urls_not_resolve(self): + response = self.client.get('/no_overlapping_view/sometitle/') + self.assertEqual(response.status_code, 404) + + def test_nested_overlapping_urls_reverse(self): + from django.core import urlresolvers + url = urlresolvers.reverse('nested_overlapping_view1', kwargs={'title': 'sometitle'}) + self.assertEqual(url, '/nested/overlapping_view/sometitle/') + url = urlresolvers.reverse('nested_overlapping_view2', kwargs={'author': 'someauthor'}) + self.assertEqual(url, '/nested/overlapping_view/someauthor/') + + def test_nested_overlapping_urls_resolve(self): + response = self.client.get('/nested/overlapping_view/sometitle/') + self.assertContains(response, 'overlapping_view2') + + def test_nested_overlapping_urls_not_resolve(self): + response = self.client.get('/nested/no_overlapping_view/sometitle/') + self.assertEqual(response.status_code, 404) --- a/tests/view_tests/generic_urls.py +++ b/tests/view_tests/generic_urls.py @@ -1,7 +1,7 @@ # -*- coding:utf-8 -*- from __future__ import absolute_import, unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import patterns, url, include from django.views.generic import RedirectView from . import views @@ -54,4 +54,14 @@ urlpatterns += patterns('view_tests.views', (r'^shortcuts/render/status/$', 'render_view_with_status'), (r'^shortcuts/render/current_app/$', 'render_view_with_current_app'), (r'^shortcuts/render/current_app_conflict/$', 'render_view_with_current_app_conflict'), + (r'^nested/', include(patterns('view_tests.views', + url(r'^overlapping_view/(?P