Commit e0fb90b2 authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #13922 -- Updated resolve() to support namespaces. Thanks to Nowell...

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
parent aa93f8c2
Loading
Loading
Loading
Loading
+32 −3
Original line number Diff line number Diff line
@@ -30,6 +30,35 @@ _prefixes = {}
# Overridden URLconfs for each thread are stored here.
_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):
    pass

@@ -120,7 +149,7 @@ class RegexURLPattern(object):
            # In both cases, pass any extra_kwargs as **kwargs.
            kwargs.update(self.default_args)

            return self.callback, args, kwargs
            return ResolverMatch(self.callback, args, kwargs, self.name)

    def _get_callback(self):
        if self._callback is not None:
@@ -224,9 +253,9 @@ class RegexURLResolver(object):
                    if sub_match:
                        sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
                        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
                        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)
            raise Resolver404({'tried': tried, 'path': new_path})
        raise Resolver404({'path' : path})
+71 −7
Original line number Diff line number Diff line
@@ -827,17 +827,80 @@ namespaces into URLs on specific application instances, according to the
resolve()
---------

The :func:`django.core.urlresolvers.resolve` function can be used for resolving
URL paths to the corresponding view functions. It has the following signature:
The :func:`django.core.urlresolvers.resolve` function can be used for
resolving URL paths to the corresponding view functions. It has the
following signature:

.. function:: resolve(path, urlconf=None)

``path`` is the URL path you want to resolve. As with ``reverse()`` above, you
don't need to worry about the ``urlconf`` parameter. The function returns the
triple (view function, arguments, keyword arguments).
``path`` is the URL path you want to resolve. As with
:func:`~django.core.urlresolvers.reverse`, you don't need to
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``
error before redirecting to it::
.. class:: ResolverMatch()

    .. 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 django.core.urlresolvers import resolve
@@ -858,6 +921,7 @@ error before redirecting to it::
            return HttpResponseRedirect('/')
        return response


permalink()
-----------

+4 −0
Original line number Diff line number Diff line
@@ -7,7 +7,11 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
    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'^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'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
    (r'^ns-included4/', include('regressiontests.urlpatterns_reverse.namespace_urls', namespace='inc-ns4')),
)
+3 −0
Original line number Diff line number Diff line
@@ -23,6 +23,9 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
    url(r'^normal/$', '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'^test2/', include(testobj2.urls)),
    (r'^default/', include(default_testobj.urls)),
+62 −1
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@ import unittest

from django.conf import settings
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.shortcuts import redirect
from django.test import TestCase
@@ -26,6 +26,35 @@ from django.test import TestCase
import urlconf_outer
import urlconf_inner
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 = (
    ('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/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):
        "A default application namespace can be used for lookup"
        self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
@@ -317,3 +352,29 @@ class NoRootUrlConfTests(TestCase):

    def test_no_handler_exception(self):
        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)