diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index e6ef6e2f9e..f144ce4bb1 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -68,6 +68,9 @@ class BaseHandler(object): from django.core import exceptions, urlresolvers from django.conf import settings + # Reset the urlconf for this thread. + urlresolvers.set_urlconf(None) + # Apply request middleware for middleware_method in self._request_middleware: response = middleware_method(request) @@ -77,61 +80,69 @@ class BaseHandler(object): # Get urlconf from request object, if available. Otherwise use default. urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF) + # Set the urlconf for this thread to the one specified above. + urlresolvers.set_urlconf(urlconf) + resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) try: - callback, callback_args, callback_kwargs = resolver.resolve( - request.path_info) - - # Apply view middleware - for middleware_method in self._view_middleware: - response = middleware_method(request, callback, callback_args, callback_kwargs) - if response: - return response - try: - response = callback(request, *callback_args, **callback_kwargs) - except Exception, 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) + callback, callback_args, callback_kwargs = resolver.resolve( + request.path_info) + + # Apply view middleware + for middleware_method in self._view_middleware: + response = middleware_method(request, callback, callback_args, callback_kwargs) if response: return response - raise - # Complain if the view returned None (a common error). - if response is None: try: - view_name = callback.func_name # If it's a function - except AttributeError: - view_name = callback.__class__.__name__ + '.__call__' # If it's a class - raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name) + response = callback(request, *callback_args, **callback_kwargs) + except Exception, 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: + return response + raise - return response - except http.Http404, e: - if settings.DEBUG: - from django.views import debug - return debug.technical_404_response(request, e) - else: - try: - callback, param_dict = resolver.resolve404() - return callback(request, **param_dict) - except: + # Complain if the view returned None (a common error). + if response is None: try: - return self.handle_uncaught_exception(request, resolver, sys.exc_info()) - finally: - receivers = signals.got_request_exception.send(sender=self.__class__, request=request) - except exceptions.PermissionDenied: - return http.HttpResponseForbidden('

Permission denied

') - except SystemExit: - # Allow sys.exit() to actually exit. See tickets #1023 and #4701 - raise - except: # Handle everything else, including SuspiciousOperation, etc. - # Get the exception info now, in case another exception is thrown later. - exc_info = sys.exc_info() - receivers = signals.got_request_exception.send(sender=self.__class__, request=request) - return self.handle_uncaught_exception(request, resolver, exc_info) + view_name = callback.func_name # If it's a function + except AttributeError: + view_name = callback.__class__.__name__ + '.__call__' # If it's a class + raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name) + + return response + except http.Http404, e: + if settings.DEBUG: + from django.views import debug + return debug.technical_404_response(request, e) + else: + try: + callback, param_dict = resolver.resolve404() + return callback(request, **param_dict) + except: + try: + return self.handle_uncaught_exception(request, resolver, sys.exc_info()) + finally: + receivers = signals.got_request_exception.send(sender=self.__class__, request=request) + except exceptions.PermissionDenied: + return http.HttpResponseForbidden('

Permission denied

') + except SystemExit: + # Allow sys.exit() to actually exit. See tickets #1023 and #4701 + raise + except: # Handle everything else, including SuspiciousOperation, etc. + # Get the exception info now, in case another exception is thrown later. + exc_info = sys.exc_info() + receivers = signals.got_request_exception.send(sender=self.__class__, request=request) + return self.handle_uncaught_exception(request, resolver, exc_info) + finally: + # Reset URLconf for this thread on the way out for complete + # isolation of request.urlconf + urlresolvers.set_urlconf(None) def handle_uncaught_exception(self, request, resolver, exc_info): """ diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index eedb8f126c..a924afeaf8 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -10,6 +10,7 @@ a string) and returns a tuple in this format: import re from django.http import Http404 +from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.utils.datastructures import MultiValueDict from django.utils.encoding import iri_to_uri, force_unicode, smart_str @@ -32,6 +33,9 @@ _callable_cache = {} # Maps view and url pattern names to their view functions. # be empty. _prefixes = {} +# Overridden URLconfs for each thread are stored here. +_urlconfs = {} + class Resolver404(Http404): pass @@ -300,9 +304,13 @@ class RegexURLResolver(object): "arguments '%s' not found." % (lookup_view_s, args, kwargs)) def resolve(path, urlconf=None): + if urlconf is None: + urlconf = get_urlconf() return get_resolver(urlconf).resolve(path) def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): + if urlconf is None: + urlconf = get_urlconf() resolver = get_resolver(urlconf) args = args or [] kwargs = kwargs or {} @@ -371,3 +379,25 @@ def get_script_prefix(): """ return _prefixes.get(currentThread(), u'/') +def set_urlconf(urlconf_name): + """ + Sets the URLconf for the current thread (overriding the default one in + settings). Set to None to revert back to the default. + """ + thread = currentThread() + if urlconf_name: + _urlconfs[thread] = urlconf_name + else: + # faster than wrapping in a try/except + if thread in _urlconfs: + del _urlconfs[thread] + +def get_urlconf(default=None): + """ + Returns the root URLconf to use for the current thread if it has been + changed from the default one. + """ + thread = currentThread() + if thread in _urlconfs: + return _urlconfs[thread] + return default diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 0b2257cefe..fd45e79876 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -40,7 +40,8 @@ algorithm the system follows to determine which Python code to execute: 1. Django determines the root URLconf module to use. Ordinarily, this is the value of the ``ROOT_URLCONF`` setting, but if the incoming - ``HttpRequest`` object has an attribute called ``urlconf``, its value + ``HttpRequest`` object has an attribute called ``urlconf`` (set by + middleware :ref:`request processing `), its value will be used in place of the ``ROOT_URLCONF`` setting. 2. Django loads that Python module and looks for the variable diff --git a/tests/regressiontests/urlpatterns_reverse/middleware.py b/tests/regressiontests/urlpatterns_reverse/middleware.py new file mode 100644 index 0000000000..7bb9aa924f --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/middleware.py @@ -0,0 +1,7 @@ +from django.core.urlresolvers import set_urlconf + +import urlconf_inner + +class ChangeURLconfMiddleware(object): + def process_request(self, request): + request.urlconf = urlconf_inner.__name__ diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index d4f281ba81..34c9db25d3 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -16,11 +16,16 @@ ImproperlyConfigured: The included urlconf regressiontests.urlpatterns_reverse.n import unittest +from django.conf import settings from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404 from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.shortcuts import redirect from django.test import TestCase +import urlconf_outer +import urlconf_inner +import middleware + test_data = ( ('places', '/places/3/', [3], {}), ('places', '/places/3/', ['3'], {}), @@ -239,3 +244,35 @@ class NamespaceTests(TestCase): self.assertEquals('/other1/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42], current_app='other-ns1')) self.assertEquals('/other1/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='other-ns1')) + +class RequestURLconfTests(TestCase): + def setUp(self): + self.root_urlconf = settings.ROOT_URLCONF + self.middleware_classes = settings.MIDDLEWARE_CLASSES + settings.ROOT_URLCONF = urlconf_outer.__name__ + + def tearDown(self): + settings.ROOT_URLCONF = self.root_urlconf + settings.MIDDLEWARE_CLASSES = self.middleware_classes + + def test_urlconf(self): + response = self.client.get('/test/me/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'outer:/test/me/,' + 'inner:/inner_urlconf/second_test/') + response = self.client.get('/inner_urlconf/second_test/') + self.assertEqual(response.status_code, 200) + response = self.client.get('/second_test/') + self.assertEqual(response.status_code, 404) + + def test_urlconf_overridden(self): + settings.MIDDLEWARE_CLASSES += ( + '%s.ChangeURLconfMiddleware' % middleware.__name__, + ) + response = self.client.get('/test/me/') + self.assertEqual(response.status_code, 404) + response = self.client.get('/inner_urlconf/second_test/') + self.assertEqual(response.status_code, 404) + response = self.client.get('/second_test/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'outer:,inner:/second_test/') diff --git a/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py b/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py new file mode 100644 index 0000000000..d188e06421 --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py @@ -0,0 +1,12 @@ +from django.conf.urls.defaults import * +from django.template import Template, Context +from django.http import HttpResponse + +def inner_view(request): + content = Template('{% url outer as outer_url %}outer:{{ outer_url }},' + '{% url inner as inner_url %}inner:{{ inner_url }}').render(Context()) + return HttpResponse(content) + +urlpatterns = patterns('', + url(r'^second_test/$', inner_view, name='inner'), +) \ No newline at end of file diff --git a/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py b/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py new file mode 100644 index 0000000000..506e03666e --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import * + +import urlconf_inner + + +urlpatterns = patterns('', + url(r'^test/me/$', urlconf_inner.inner_view, name='outer'), + url(r'^inner_urlconf/', include(urlconf_inner.__name__)) +) \ No newline at end of file