How to handle complex URL in a elegant way?

pyramid, python

Solution

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.

Problem

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?

Original source