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?