How to handle complex URL in a elegant way?

I’m writing a admin website which control several websites with same program and database schema but different content. The URL I designed like this:

http://example.com/site                 A list of all sites which under control
http://example.com/site/{id}            A brief overview of select site with ID id
http://example.com/site/{id}/user       User list of target site
http://example.com/site/{id}/item       A list of items sold on target site
http://example.com/site/{id}/item/{iid} Item detailed information
# ...... something similar

As you can see, nearly all URL are need the site_id. And in almost all views, I have to do some common jobs like query Site model against database with the site_id. Also, I have to pass site_id whenever I invoke request.route_path.

So… is there anyway for me to make my life easier?

Best answer

It might be useful for you to use a hybrid approach to get the site loaded.

def groupfinder(userid, request):
    user = request.db.query(User).filter_by(id=userid).first()
    if user is not None:
        # somehow get the list of sites they are members
        sites = user.allowed_sites
        return ['site:%d' % s.id for s in sites]

class SiteFactory(object):
    def __init__(self, request):
        self.request = request

    def __getitem__(self, key):
        site = self.request.db.query(Site).filter_by(id=key).first()
        if site is None:
            raise KeyError
        site.__parent__ = self
        site.__name__ = key
        site.__acl__ = [
            (Allow, 'site:%d' % site.id, 'view'),
        ]
        return site

We’ll use the groupfinder to map users to principals. We’ve chosen here to only map them to the sites which they have a membership within. Our simple traversal only requires a root object. It updates the loaded site with an __acl__ that uses the same principals the groupfinder is setup to create.

You’ll need to setup the request.db given patterns in the Pyramid Cookbook.

def site_pregenerator(request, elements, kw):
    # request.route_url(route_name, *elements, **kw)
    from pyramid.traversal import find_interface
    # we use find_interface in case we improve our hybrid traversal process
    # to take us deeper into the hierarchy, where Site might be context.__parent__
    site = find_interface(request.context, Site)
    if site is not None:
        kw['site_id'] = site.id
    return elements, kw

Pregenerator can find the site_id and add it to URLs for you automatically.

def add_site_route(config, name, pattern, **kw):
    kw['traverse'] = '/{site_id}'
    kw['factory'] = SiteFactory
    kw['pregenerator'] = site_pregenerator

    if pattern.startswith('/'):
        pattern = pattern[1:]
    config.add_route(name, '/site/{site_id}/' + pattern, **kw)

def main(global_conf, **settings):
    config = Configurator(settings=settings)

    authn_policy = AuthTktAuthenticationPolicy('seekrit', callback=groupfinder)
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(ACLAuthorizationPolicy())

    config.add_directive(add_site_route, 'add_site_route')

    config.include(site_routes)
    config.scan()
    return config.make_wsgi_app()

def site_routes(config):
    config.add_site_route('site_users', '/user')
    config.add_site_route('site_items', '/items')

We setup our application here. We also moved the routes into an includable function which can allow us to more easily test the routes.

@view_config(route_name='site_users', permission='view')
def users_view(request):
    site = request.context

Our views are then simplified. They are only invoked if the user has permission to access the site, and the site object is already loaded for us.

Hybrid Traversal

A custom directive add_site_route is added to enhance your config object with a wrapper around add_route which will automatically add traversal support to the route. When that route is matched, it will take the {site_id} placeholder from the route pattern and use that as your traversal path (/{site_id} is path we define based on how our traversal tree is structured).

Traversal happens on the path /{site_id} where the first step is finding the root of the tree (/). The route is setup to perform traversal using the SiteFactory as the root of the traversal path. This class is instantiated as the root, and the __getitem__ is invoked with the key which is the next segment in the path ({site_id}). We then find a site object matching that key and load it if possible. The site object is then updated with a __parent__ and __name__ to allow the find_interface to work. It is also enhanced with an __acl__ providing permissions mentioned later.

Pregenerator

Each route is updated with a pregenerator that attempts to find the instance of Site in the traversal hierarchy for a request. This could fail if the current request did not resolve to a site-based URL. The pregenerator then updates the keywords sent to route_url with the site id.

Authentication

The example shows how you can have an authentication policy which maps a user into principals indicating that this user is in the “site:” group. The site (request.context) is then updated to have an ACL saying that if site.id == 1 someone in the “site:1” group should have the “view” permission. The users_view is then updated to require the “view” permission. This will raise an HTTPForbidden exception if the user is denied access to the view. You can write an exception view to conditionally translate this into a 404 if you want.

The purpose of my answer is just to show how a hybrid approach can make your views a little nicer by handling common parts of a URL in the background. HTH.