Fixed #5034 -- honor request.urlconf in reverse and resolve.

This enables {% url %} to honor request.urlconf set from process_request
middleware methods.

Thanks SmileyChris for the initial patch work.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@11740 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Brian Rosner 2009-11-16 01:58:00 +00:00
parent c169f8cb17
commit 6c61ca3d74
7 changed files with 154 additions and 47 deletions

View File

@ -68,6 +68,9 @@ class BaseHandler(object):
from django.core import exceptions, urlresolvers from django.core import exceptions, urlresolvers
from django.conf import settings from django.conf import settings
# Reset the urlconf for this thread.
urlresolvers.set_urlconf(None)
# Apply request middleware # Apply request middleware
for middleware_method in self._request_middleware: for middleware_method in self._request_middleware:
response = middleware_method(request) response = middleware_method(request)
@ -77,61 +80,69 @@ class BaseHandler(object):
# Get urlconf from request object, if available. Otherwise use default. # Get urlconf from request object, if available. Otherwise use default.
urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF) 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) resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)
try: 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: try:
response = callback(request, *callback_args, **callback_kwargs) callback, callback_args, callback_kwargs = resolver.resolve(
except Exception, e: request.path_info)
# If the view raised an exception, run it through exception
# middleware, and if the exception middleware returns a # Apply view middleware
# response, use that. Otherwise, reraise the exception. for middleware_method in self._view_middleware:
for middleware_method in self._exception_middleware: response = middleware_method(request, callback, callback_args, callback_kwargs)
response = middleware_method(request, e)
if response: if response:
return response return response
raise
# Complain if the view returned None (a common error).
if response is None:
try: try:
view_name = callback.func_name # If it's a function response = callback(request, *callback_args, **callback_kwargs)
except AttributeError: except Exception, e:
view_name = callback.__class__.__name__ + '.__call__' # If it's a class # If the view raised an exception, run it through exception
raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name) # 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 # Complain if the view returned None (a common error).
except http.Http404, e: if response is None:
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: try:
return self.handle_uncaught_exception(request, resolver, sys.exc_info()) view_name = callback.func_name # If it's a function
finally: except AttributeError:
receivers = signals.got_request_exception.send(sender=self.__class__, request=request) view_name = callback.__class__.__name__ + '.__call__' # If it's a class
except exceptions.PermissionDenied: raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name)
return http.HttpResponseForbidden('<h1>Permission denied</h1>')
except SystemExit: return response
# Allow sys.exit() to actually exit. See tickets #1023 and #4701 except http.Http404, e:
raise if settings.DEBUG:
except: # Handle everything else, including SuspiciousOperation, etc. from django.views import debug
# Get the exception info now, in case another exception is thrown later. return debug.technical_404_response(request, e)
exc_info = sys.exc_info() else:
receivers = signals.got_request_exception.send(sender=self.__class__, request=request) try:
return self.handle_uncaught_exception(request, resolver, exc_info) 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('<h1>Permission denied</h1>')
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): def handle_uncaught_exception(self, request, resolver, exc_info):
""" """

View File

@ -10,6 +10,7 @@ a string) and returns a tuple in this format:
import re import re
from django.http import Http404 from django.http import Http404
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.encoding import iri_to_uri, force_unicode, smart_str 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. # be empty.
_prefixes = {} _prefixes = {}
# Overridden URLconfs for each thread are stored here.
_urlconfs = {}
class Resolver404(Http404): class Resolver404(Http404):
pass pass
@ -300,9 +304,13 @@ class RegexURLResolver(object):
"arguments '%s' not found." % (lookup_view_s, args, kwargs)) "arguments '%s' not found." % (lookup_view_s, args, kwargs))
def resolve(path, urlconf=None): def resolve(path, urlconf=None):
if urlconf is None:
urlconf = get_urlconf()
return get_resolver(urlconf).resolve(path) return get_resolver(urlconf).resolve(path)
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): 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) resolver = get_resolver(urlconf)
args = args or [] args = args or []
kwargs = kwargs or {} kwargs = kwargs or {}
@ -371,3 +379,25 @@ def get_script_prefix():
""" """
return _prefixes.get(currentThread(), u'/') 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

View File

@ -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, 1. Django determines the root URLconf module to use. Ordinarily,
this is the value of the ``ROOT_URLCONF`` setting, but if the incoming 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 <request-middleware>`), its value
will be used in place of the ``ROOT_URLCONF`` setting. will be used in place of the ``ROOT_URLCONF`` setting.
2. Django loads that Python module and looks for the variable 2. Django loads that Python module and looks for the variable

View File

@ -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__

View File

@ -16,11 +16,16 @@ ImproperlyConfigured: The included urlconf regressiontests.urlpatterns_reverse.n
import unittest import unittest
from django.conf import settings
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404 from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.test import TestCase from django.test import TestCase
import urlconf_outer
import urlconf_inner
import middleware
test_data = ( test_data = (
('places', '/places/3/', [3], {}), ('places', '/places/3/', [3], {}),
('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/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')) 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/')

View File

@ -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'),
)

View File

@ -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__))
)