mirror of https://github.com/django/django.git
Fixed #13922 -- Updated resolve() to support namespaces. Thanks to Nowell Strite for the report and patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@13479 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
aa93f8c2f0
commit
e0fb90b2f3
|
@ -30,6 +30,35 @@ _prefixes = {}
|
||||||
# Overridden URLconfs for each thread are stored here.
|
# Overridden URLconfs for each thread are stored here.
|
||||||
_urlconfs = {}
|
_urlconfs = {}
|
||||||
|
|
||||||
|
class ResolverMatch(object):
|
||||||
|
def __init__(self, func, args, kwargs, url_name=None, app_name=None, namespaces=None):
|
||||||
|
self.func = func
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.app_name = app_name
|
||||||
|
if namespaces:
|
||||||
|
self.namespaces = [x for x in namespaces if x]
|
||||||
|
else:
|
||||||
|
self.namespaces = []
|
||||||
|
if not url_name:
|
||||||
|
url_name = '.'.join([ func.__module__, func.__name__ ])
|
||||||
|
self.url_name = url_name
|
||||||
|
|
||||||
|
def namespace(self):
|
||||||
|
return ':'.join(self.namespaces)
|
||||||
|
namespace = property(namespace)
|
||||||
|
|
||||||
|
def view_name(self):
|
||||||
|
return ':'.join([ x for x in [ self.namespace, self.url_name ] if x ])
|
||||||
|
view_name = property(view_name)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return (self.func, self.args, self.kwargs)[index]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name='%s', app_name='%s', namespace='%s')" % (
|
||||||
|
self.func, self.args, self.kwargs, self.url_name, self.app_name, self.namespace)
|
||||||
|
|
||||||
class Resolver404(Http404):
|
class Resolver404(Http404):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -120,7 +149,7 @@ class RegexURLPattern(object):
|
||||||
# In both cases, pass any extra_kwargs as **kwargs.
|
# In both cases, pass any extra_kwargs as **kwargs.
|
||||||
kwargs.update(self.default_args)
|
kwargs.update(self.default_args)
|
||||||
|
|
||||||
return self.callback, args, kwargs
|
return ResolverMatch(self.callback, args, kwargs, self.name)
|
||||||
|
|
||||||
def _get_callback(self):
|
def _get_callback(self):
|
||||||
if self._callback is not None:
|
if self._callback is not None:
|
||||||
|
@ -224,9 +253,9 @@ class RegexURLResolver(object):
|
||||||
if sub_match:
|
if sub_match:
|
||||||
sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
|
sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
|
||||||
sub_match_dict.update(self.default_kwargs)
|
sub_match_dict.update(self.default_kwargs)
|
||||||
for k, v in sub_match[2].iteritems():
|
for k, v in sub_match.kwargs.iteritems():
|
||||||
sub_match_dict[smart_str(k)] = v
|
sub_match_dict[smart_str(k)] = v
|
||||||
return sub_match[0], sub_match[1], sub_match_dict
|
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.regex.pattern)
|
tried.append(pattern.regex.pattern)
|
||||||
raise Resolver404({'tried': tried, 'path': new_path})
|
raise Resolver404({'tried': tried, 'path': new_path})
|
||||||
raise Resolver404({'path' : path})
|
raise Resolver404({'path' : path})
|
||||||
|
|
|
@ -827,17 +827,80 @@ namespaces into URLs on specific application instances, according to the
|
||||||
resolve()
|
resolve()
|
||||||
---------
|
---------
|
||||||
|
|
||||||
The :func:`django.core.urlresolvers.resolve` function can be used for resolving
|
The :func:`django.core.urlresolvers.resolve` function can be used for
|
||||||
URL paths to the corresponding view functions. It has the following signature:
|
resolving URL paths to the corresponding view functions. It has the
|
||||||
|
following signature:
|
||||||
|
|
||||||
.. function:: resolve(path, urlconf=None)
|
.. function:: resolve(path, urlconf=None)
|
||||||
|
|
||||||
``path`` is the URL path you want to resolve. As with ``reverse()`` above, you
|
``path`` is the URL path you want to resolve. As with
|
||||||
don't need to worry about the ``urlconf`` parameter. The function returns the
|
:func:`~django.core.urlresolvers.reverse`, you don't need to
|
||||||
triple (view function, arguments, keyword arguments).
|
worry about the ``urlconf`` parameter. The function returns a
|
||||||
|
:class:`django.core.urlresolvers.ResolverMatch` object that allows you
|
||||||
|
to access various meta-data about the resolved URL.
|
||||||
|
|
||||||
For example, it can be used for testing if a view would raise a ``Http404``
|
.. class:: ResolverMatch()
|
||||||
error before redirecting to it::
|
|
||||||
|
.. attribute:: ResolverMatch.func
|
||||||
|
|
||||||
|
The view function that would be used to serve the URL
|
||||||
|
|
||||||
|
.. attribute:: ResolverMatch.args
|
||||||
|
|
||||||
|
The arguments that would be passed to the view function, as
|
||||||
|
parsed from the URL.
|
||||||
|
|
||||||
|
.. attribute:: ResolverMatch.kwargs
|
||||||
|
|
||||||
|
The keyword arguments that would be passed to the view
|
||||||
|
function, as parsed from the URL.
|
||||||
|
|
||||||
|
.. attribute:: ResolverMatch.url_name
|
||||||
|
|
||||||
|
The name of the URL pattern that matches the URL.
|
||||||
|
|
||||||
|
.. attribute:: ResolverMatch.app_name
|
||||||
|
|
||||||
|
The application namespace for the URL pattern that matches the
|
||||||
|
URL.
|
||||||
|
|
||||||
|
.. attribute:: ResolverMatch.namespace
|
||||||
|
|
||||||
|
The instance namespace for the URL pattern that matches the
|
||||||
|
URL.
|
||||||
|
|
||||||
|
.. attribute:: ResolverMatch.namespaces
|
||||||
|
|
||||||
|
The list of individual namespace components in the full
|
||||||
|
instance namespace for the URL pattern that matches the URL.
|
||||||
|
i.e., if the namespace is ``foo:bar``, then namespaces will be
|
||||||
|
``[`foo`, `bar`]``.
|
||||||
|
|
||||||
|
A :class:`~django.core.urlresolvers.ResolverMatch` object can then be
|
||||||
|
interrogated to provide information about the URL pattern that matches
|
||||||
|
a URL::
|
||||||
|
|
||||||
|
# Resolve a URL
|
||||||
|
match = resolve('/some/path/')
|
||||||
|
# Print the URL pattern that matches the URL
|
||||||
|
print match.url_name
|
||||||
|
|
||||||
|
A :class:`~django.core.urlresolvers.ResolverMatch` object can also be
|
||||||
|
assigned to a triple::
|
||||||
|
|
||||||
|
func, args, kwargs = resolve('/some/path/')
|
||||||
|
|
||||||
|
.. versionchanged:: 1.3
|
||||||
|
Triple-assignment exists for backwards-compatibility. Prior to
|
||||||
|
Django 1.3, :func:`~django.core.urlresolvers.resolve` returned a
|
||||||
|
triple containing (view function, arguments, keyword arguments);
|
||||||
|
the :class:`~django.core.urlresolvers.ResolverMatch` object (as
|
||||||
|
well as the namespace and pattern information it provides) is not
|
||||||
|
available in earlier Django releases.
|
||||||
|
|
||||||
|
One possible use of :func:`~django.core.urlresolvers.resolve` would be
|
||||||
|
to testing if a view would raise a ``Http404`` error before
|
||||||
|
redirecting to it::
|
||||||
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
from django.core.urlresolvers import resolve
|
from django.core.urlresolvers import resolve
|
||||||
|
@ -858,6 +921,7 @@ error before redirecting to it::
|
||||||
return HttpResponseRedirect('/')
|
return HttpResponseRedirect('/')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
permalink()
|
permalink()
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,11 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
|
||||||
url(r'^normal/$', 'empty_view', name='inc-normal-view'),
|
url(r'^normal/$', 'empty_view', name='inc-normal-view'),
|
||||||
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
|
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
|
||||||
|
|
||||||
|
url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-mixed-args'),
|
||||||
|
url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='inc-no-kwargs'),
|
||||||
|
|
||||||
(r'^test3/', include(testobj3.urls)),
|
(r'^test3/', include(testobj3.urls)),
|
||||||
(r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
|
(r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
|
||||||
|
(r'^ns-included4/', include('regressiontests.urlpatterns_reverse.namespace_urls', namespace='inc-ns4')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
|
||||||
url(r'^normal/$', 'empty_view', name='normal-view'),
|
url(r'^normal/$', 'empty_view', name='normal-view'),
|
||||||
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
|
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
|
||||||
|
|
||||||
|
url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='mixed-args'),
|
||||||
|
url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='no-kwargs'),
|
||||||
|
|
||||||
(r'^test1/', include(testobj1.urls)),
|
(r'^test1/', include(testobj1.urls)),
|
||||||
(r'^test2/', include(testobj2.urls)),
|
(r'^test2/', include(testobj2.urls)),
|
||||||
(r'^default/', include(default_testobj.urls)),
|
(r'^default/', include(default_testobj.urls)),
|
||||||
|
|
|
@ -18,7 +18,7 @@ import unittest
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404
|
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404, ResolverMatch
|
||||||
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
|
||||||
|
@ -26,6 +26,35 @@ from django.test import TestCase
|
||||||
import urlconf_outer
|
import urlconf_outer
|
||||||
import urlconf_inner
|
import urlconf_inner
|
||||||
import middleware
|
import middleware
|
||||||
|
import views
|
||||||
|
|
||||||
|
resolve_test_data = (
|
||||||
|
# These entries are in the format: (path, url_name, app_name, namespace, view_func, args, kwargs)
|
||||||
|
# Simple case
|
||||||
|
('/normal/42/37/', 'normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
('/included/normal/42/37/', 'inc-normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
|
||||||
|
# Unnamed args are dropped if you have *any* kwargs in a pattern
|
||||||
|
('/mixed_args/42/37/', 'mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
|
||||||
|
('/included/mixed_args/42/37/', 'inc-mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
|
||||||
|
|
||||||
|
# If you have no kwargs, you get an args list.
|
||||||
|
('/no_kwargs/42/37/', 'no-kwargs', None, '', views.empty_view, ('42','37'), {}),
|
||||||
|
('/included/no_kwargs/42/37/', 'inc-no-kwargs', None, '', views.empty_view, ('42','37'), {}),
|
||||||
|
|
||||||
|
# Namespaces
|
||||||
|
('/test1/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
('/ns-included1/normal/42/37/', 'inc-normal-view', None, 'inc-ns1', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
('/default/inner/42/37/', 'urlobject-view', 'testapp', 'testapp', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
('/other2/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns2', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
('/other1/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
|
||||||
|
|
||||||
|
# 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'}),
|
||||||
|
)
|
||||||
|
|
||||||
test_data = (
|
test_data = (
|
||||||
('places', '/places/3/', [3], {}),
|
('places', '/places/3/', [3], {}),
|
||||||
|
@ -229,6 +258,12 @@ class NamespaceTests(TestCase):
|
||||||
self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
|
self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
|
||||||
self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
|
self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
|
||||||
|
|
||||||
|
def test_nested_namespace_pattern(self):
|
||||||
|
"Namespaces can be nested"
|
||||||
|
self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view'))
|
||||||
|
self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/37/42/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view', args=[37,42]))
|
||||||
|
self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/42/37/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
|
||||||
|
|
||||||
def test_app_lookup_object(self):
|
def test_app_lookup_object(self):
|
||||||
"A default application namespace can be used for lookup"
|
"A default application namespace can be used for lookup"
|
||||||
self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
|
self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
|
||||||
|
@ -317,3 +352,29 @@ class NoRootUrlConfTests(TestCase):
|
||||||
|
|
||||||
def test_no_handler_exception(self):
|
def test_no_handler_exception(self):
|
||||||
self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/')
|
self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/')
|
||||||
|
|
||||||
|
class ResolverMatchTests(TestCase):
|
||||||
|
urls = 'regressiontests.urlpatterns_reverse.namespace_urls'
|
||||||
|
|
||||||
|
def test_urlpattern_resolve(self):
|
||||||
|
for path, name, app_name, namespace, func, args, kwargs in resolve_test_data:
|
||||||
|
# Test legacy support for extracting "function, args, kwargs"
|
||||||
|
match_func, match_args, match_kwargs = resolve(path)
|
||||||
|
self.assertEqual(match_func, func)
|
||||||
|
self.assertEqual(match_args, args)
|
||||||
|
self.assertEqual(match_kwargs, kwargs)
|
||||||
|
|
||||||
|
# Test ResolverMatch capabilities.
|
||||||
|
match = resolve(path)
|
||||||
|
self.assertEqual(match.__class__, ResolverMatch)
|
||||||
|
self.assertEqual(match.url_name, name)
|
||||||
|
self.assertEqual(match.args, args)
|
||||||
|
self.assertEqual(match.kwargs, kwargs)
|
||||||
|
self.assertEqual(match.app_name, app_name)
|
||||||
|
self.assertEqual(match.namespace, namespace)
|
||||||
|
self.assertEqual(match.func, func)
|
||||||
|
|
||||||
|
# ... and for legacy purposes:
|
||||||
|
self.assertEquals(match[0], func)
|
||||||
|
self.assertEquals(match[1], args)
|
||||||
|
self.assertEquals(match[2], kwargs)
|
||||||
|
|
Loading…
Reference in New Issue