restontrac / trac-dev / restontrac / tracrest /

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

# Copyright 2009-2011 Olemis Lang <olemis at>
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

r"""Publish Trac data via a REST-ful interface

RESTful interface built on top of Trac RpcPlugin

Copyright 2009-2011 Olemis Lang <olemis at>
Licensed under the Apache License
from routes.mapper import Mapper
from routes.mapper import strip_slashes
from routes import request_config

class RestMapper(Mapper):
    """ Specialization of route.Mapper to allow
        a more customizable behaviour of the resource()
        operation used to automatically configure the set
        of url mappings generally allowed in for a
        collection/member resource
    #TODO document the objective of this re-writing, evaluate the creation of a fork of routes in bb instead
    def resource(self, member_name, collection_name, **kwargs):
        """Generate routes for a controller resource

        The member_name name should be the appropriate singular version
        of the resource given your locale and used with members of the
        collection. The collection_name name will be used to refer to
        the resource collection methods and should be a plural version
        of the member_name argument. By default, the member_name name
        will also be assumed to map to a controller you create.

        The concept of a web resource maps somewhat directly to 'CRUD'
        operations. The overlying things to keep in mind is that
        mapping a resource is about handling creating, viewing, and
        editing that resource.

        All keyword arguments are optional.

            If specified in the keyword args, the controller will be
            the actual controller used, but the rest of the naming
            conventions used for the route names and URL paths are

            Additional action mappings used to manipulate/view the
            entire set of resources provided by the controller.


                map.resource('message', 'messages', collection={'rss':'GET'})
                # GET /message/rss (maps to the rss action)
                # also adds named route "rss_message"

            Additional action mappings used to access an individual
            'member' of this controllers resources.


                map.resource('message', 'messages', member={'mark':'POST'})
                # POST /message/1/mark (maps to the mark action)
                # also adds named route "mark_message"

            Action mappings that involve dealing with a new member in
            the controller resources.


                map.resource('message', 'messages', new={'preview':'POST'})
                # POST /message/new/preview (maps to the preview action)
                # also adds a url named "preview_new_message"

            Prepends the URL path for the Route with the path_prefix
            given. This is most useful for cases where you want to mix
            resources or relations between resources.

            Perpends the route names that are generated with the
            name_prefix given. Combined with the path_prefix option,
            it's easy to generate route names and paths that represent
            resources that are in relations.


                map.resource('message', 'messages', controller='categories',
                # GET /category/7/message/1
                # has named route "category_message"

            A ``dict`` containing information about the parent
            resource, for creating a nested resource. It should contain
            the ``member_name`` and ``collection_name`` of the parent
            resource. This ``dict`` will
            be available via the associated ``Route`` object which can
            be accessed during a request via

            If ``parent_resource`` is supplied and ``path_prefix``
            isn't, ``path_prefix`` will be generated from
            ``parent_resource`` as
            "<parent collection name>/:<parent member name>_id".

            If ``parent_resource`` is supplied and ``name_prefix``
            isn't, ``name_prefix`` will be generated from
            ``parent_resource`` as  "<parent member name>_".


                >>> from routes.util import url_for
                >>> m = Mapper()
                >>> m.resource('location', 'locations',
                ...            parent_resource=dict(member_name='region',
                ...                                 collection_name='regions'))
                >>> # path_prefix is "regions/:region_id"
                >>> # name prefix is "region_"
                >>> url_for('region_locations', region_id=13)
                >>> url_for('region_new_location', region_id=13)
                >>> url_for('region_location', region_id=13, id=60)
                >>> url_for('region_edit_location', region_id=13, id=60)

            Overriding generated ``path_prefix``::

                >>> m = Mapper()
                >>> m.resource('location', 'locations',
                ...            parent_resource=dict(member_name='region',
                ...                                 collection_name='regions'),
                ...            path_prefix='areas/:area_id')
                >>> # name prefix is "region_"
                >>> url_for('region_locations', area_id=51)

            Overriding generated ``name_prefix``::

                >>> m = Mapper()
                >>> m.resource('location', 'locations',
                ...            parent_resource=dict(member_name='region',
                ...                                 collection_name='regions'),
                ...            name_prefix='')
                >>> # path_prefix is "regions/:region_id"
                >>> url_for('locations', region_id=51)

        collection = kwargs.pop('collection', {})
        member = kwargs.pop('member', {})
        exclude_edit = kwargs.pop('member', {}).get('edit',"") == 'IGNORE'
        exclude_new = kwargs.pop('new', {}) == 'IGNORE'
        new = kwargs.pop('new', {})
        path_prefix = kwargs.pop('path_prefix', None)
        name_prefix = kwargs.pop('name_prefix', None)
        parent_resource = kwargs.pop('parent_resource', None)

        # Generate ``path_prefix`` if ``path_prefix`` wasn't specified and
        # ``parent_resource`` was. Likewise for ``name_prefix``. Make sure
        # that ``path_prefix`` and ``name_prefix`` *always* take precedence if
        # they are specified--in particular, we need to be careful when they
        # are explicitly set to "".
        if parent_resource is not None:
            if path_prefix is None:
                path_prefix = '%s/:%s_id' % (parent_resource['collection_name'],
            if name_prefix is None:
                name_prefix = '%s_' % parent_resource['member_name']
            if path_prefix is None: path_prefix = ''
            if name_prefix is None: name_prefix = ''

        # Ensure the edit and new actions are in and GET
        if not exclude_edit:
            member['edit'] = 'GET'
        if not exclude_new:
            new.update({'new': 'GET'})

        # Make new dict's based off the old, except the old values become keys,
        # and the old keys become items in a list as the value
        def swap(dct, newdct):
            """Swap the keys and values in the dict, and uppercase the values
            from the dict during the swap."""
            for key, val in dct.iteritems():
                newdct.setdefault(val.upper(), []).append(key)
            return newdct
        collection_methods = swap(collection, {})
        member_methods = swap(member, {})
        new_methods = swap(new, {})

        # Insert create, update, and destroy methods
        collection_methods.setdefault('POST', []).insert(0, 'create')
        member_methods.setdefault('PUT', []).insert(0, 'update')
        member_methods.setdefault('DELETE', []).insert(0, 'delete')

        # If there's a path prefix option, use it with the controller
        controller = strip_slashes(collection_name)
        path_prefix = strip_slashes(path_prefix)
        path_prefix = '/' + path_prefix
        if path_prefix and path_prefix != '/':
            path = path_prefix + '/' + controller
            path = '/' + controller
        collection_path = path
        new_path = path + "/new"
        member_path = path + "/:(id)"

        options = {
            'controller': kwargs.get('controller', controller),
            '_member_name': member_name,
            '_collection_name': collection_name,
            '_parent_resource': parent_resource,
            '_filter': kwargs.get('_filter')

        def requirements_for(meth):
            """Returns a new dict to be used for all route creation as the
            route options"""
            opts = options.copy()
            if method != 'any':
                opts['conditions'] = {'method':[meth.upper()]}
            return opts

        # Add the routes for handling collection methods
        for method, lst in collection_methods.iteritems():
            primary = (method != 'GET' and lst.pop(0)) or None
            route_options = requirements_for(method)
            for action in lst:
                route_options['action'] = action
                route_name = "%s%s_%s" % (name_prefix, action, collection_name)
                self.connect("formatted_" + route_name, "%s/%s.:(format)" %\
                                                        (collection_path, action), **route_options)
                self.connect(route_name, "%s/%s" % (collection_path, action),
            if primary:
                route_options['action'] = primary
                self.connect("%s.:(format)" % collection_path, **route_options)
                self.connect(collection_path, **route_options)

        # Specifically add in the built-in 'index' collection method and its
        # formatted version
        collection_getter_action = kwargs.pop('collection_getter_action','index')
        self.connect("formatted_" + name_prefix + collection_name,
            collection_path + ".:(format)", action=collection_getter_action,
            conditions={'method':['GET']}, **options)
        self.connect(name_prefix + collection_name, collection_path,
            action=collection_getter_action, conditions={'method':['GET']}, **options)

        if not exclude_new:
            # Add the routes that deal with new resource methods
            for method, lst in new_methods.iteritems():
                route_options = requirements_for(method)
                for action in lst:
                    path = (action == 'new' and new_path) or "%s/%s" % (new_path,
                    name = "new_" + member_name
                    if action != 'new':
                        name = action + "_" + name
                    route_options['action'] = action
                    formatted_path = (action == 'new' and new_path + '.:(format)') or\
                                     "%s/%s.:(format)" % (new_path, action)
                    self.connect("formatted_" + name_prefix + name, formatted_path,
                    self.connect(name_prefix + name, path, **route_options)

        requirements_regexp = '[^\/]+(?<!\\\)'

        # Add the routes that deal with member methods of a resource
        for method, lst in member_methods.iteritems():
            route_options = requirements_for(method)
            route_options['requirements'] = {'id':requirements_regexp}
            if method not in ['POST', 'GET', 'any']:
                primary = lst.pop(0)
                primary = None
            for action in lst:
                route_options['action'] = action
                self.connect("formatted_%s%s_%s" % (name_prefix, action,
                    "%s/%s.:(format)" % (member_path, action), **route_options)
                self.connect("%s%s_%s" % (name_prefix, action, member_name),
                    "%s/%s" % (member_path, action), **route_options)
            if primary:
                route_options['action'] = primary
                self.connect("%s.:(format)" % member_path, **route_options)
                self.connect(member_path, **route_options)

        # Specifically add the member 'show' method
        route_options = requirements_for('GET')
        route_options['action'] = kwargs.pop('member_getter_action','show')
        route_options['requirements'] = {'id':requirements_regexp}
        self.connect("formatted_" + name_prefix + member_name,
            member_path + ".:(format)", **route_options)
        self.connect(name_prefix + member_name, member_path, **route_options)

DEFAULT_ACTION_MAP = {'GET':'read','POST':'create','PUT':'update',

def map_resource_path(mapper, path, controller, methods, actions_map={}):
    """To mapp the path using a routes mapper
       methods can be a string like 'GET' or
       a comma sepparated one like 'GET,POST,PUT'
       or a list like ('GET','POST')
       actions_map is a dict with the structure {(path,HttpMethod) : actionName}
       that allows to specify custom actions mappings instead of default ones
       indicated in DEFAULT_ACTION_MAP
    #ensuring methods become iterable no matter how the values were passed
    if isinstance(methods, basestring):
        if "," in methods:
            methods = methods.split(",")
            methods = (methods,)
    #Provided http methods must be valid
    for http_method in methods:
        assert http_method in DEFAULT_ACTION_MAP.keys(), \
            "Configuration Error: Unsupported http method %s" % http_method
    actions_map = actions_map or {}
    for http_method in methods:
        # allowing custom action mappings to take place
        # if no action_map is provided, will use DEFAULT_ACTION_MAP
        action = actions_map.get((path, http_method),None)
        if not action:
            action = DEFAULT_ACTION_MAP.get(http_method, None)
        #doing the mapping once data is ready

def setup_routes_config(mapper, request, match_dict):
    """Setting up routes to enable the use
       of url_for() function when generating hyperlinks
       between resources
    config = request_config()
    config.mapper = mapper
    config.mapper_dict = match_dict = request.server_name
    config.protocol = request.scheme
    #TODO check if OK
    config.redirect = lambda url: request.redirect(url)