Commit d9c4af6b authored by Adrian Holovaty's avatar Adrian Holovaty
Browse files

Added first stab at reverse matching to urlresolvers.py

git-svn-id: http://code.djangoproject.com/svn/django/trunk@2910 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 3cef18cb
Loading
Loading
Loading
Loading
+78 −0
Original line number Diff line number Diff line
@@ -14,12 +14,56 @@ import re
class Resolver404(Http404):
    pass

class NoReverseMatch(Exception):
    pass

def get_mod_func(callback):
    # Converts 'django.views.news.stories.story_detail' to
    # ['django.views.news.stories', 'story_detail']
    dot = callback.rindex('.')
    return callback[:dot], callback[dot+1:]

class MatchChecker(object):
    "Class used in reverse RegexURLPattern lookup."
    def __init__(self, args, kwargs):
        self.args, self.kwargs = args, kwargs
        self.current_arg = 0

    def __call__(self, match_obj):
        # match_obj.group(1) is the contents of the parenthesis.
        # First we need to figure out whether it's a named or unnamed group.
        #
        grouped = match_obj.group(1)
        m = re.search(r'^\?P<(\w+)>(.*?)$', grouped)
        if m: # If this was a named group...
            # m.group(1) is the name of the group
            # m.group(2) is the regex.
            try:
                value = self.kwargs[m.group(1)]
            except KeyError:
                # It was a named group, but the arg was passed in as a
                # positional arg or not at all.
                try:
                    value = self.args[self.current_arg]
                    self.current_arg += 1
                except IndexError:
                    # The arg wasn't passed in.
                    raise NoReverseMatch('Not enough positional arguments passed in')
            test_regex = m.group(2)
        else: # Otherwise, this was a positional (unnamed) group.
            try:
                value = self.args[self.current_arg]
                self.current_arg += 1
            except IndexError:
                # The arg wasn't passed in.
                raise NoReverseMatch('Not enough positional arguments passed in')
            test_regex = grouped
        # Note we're using re.match here on purpose because the start of
        # to string needs to match.
        if not re.match(test_regex + '$', str(value)): # TODO: Unicode?
            raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
        return str(value) # TODO: Unicode?

class RegexURLPattern:
    def __init__(self, regex, callback, default_args=None):
        # regex is a string representing a regular expression.
@@ -58,12 +102,37 @@ class RegexURLPattern:
        except AttributeError, e:
            raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))

    def reverse(self, viewname, *args, **kwargs):
        if viewname != self.callback:
            raise NoReverseMatch
        return self.reverse_helper(*args, **kwargs)

    def reverse_helper(self, *args, **kwargs):
        """
        Does a "reverse" lookup -- returns the URL for the given args/kwargs.
        The args/kwargs are applied to the regular expression in this
        RegexURLPattern. For example:

            >>> RegexURLPattern('^places/(\d+)/$').reverse_helper(3)
            'places/3/'
            >>> RegexURLPattern('^places/(?P<id>\d+)/$').reverse_helper(id=3)
            'places/3/'
            >>> RegexURLPattern('^people/(?P<state>\w\w)/(\w+)/$').reverse_helper('adrian', state='il')
            'people/il/adrian/'

        Raises NoReverseMatch if the args/kwargs aren't valid for the RegexURLPattern.
        """
        # TODO: Handle nested parenthesis in the following regex.
        result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), self.regex.pattern)
        return result.replace('^', '').replace('$', '')

class RegexURLResolver(object):
    def __init__(self, regex, urlconf_name):
        # regex is a string representing a regular expression.
        # urlconf_name is a string representing the module containing urlconfs.
        self.regex = re.compile(regex)
        self.urlconf_name = urlconf_name
        self.callback = None

    def resolve(self, path):
        tried = []
@@ -110,3 +179,12 @@ class RegexURLResolver(object):

    def resolve500(self):
        return self._resolve_special('500')

    def reverse(self, viewname, *args, **kwargs):
        for pattern in self.urlconf_module.urlpatterns:
            if pattern.callback == viewname:
                try:
                    return pattern.reverse_helper(*args, **kwargs)
                except NoReverseMatch:
                    continue
        raise NoReverseMatch