255 lines
8.5 KiB
Python
255 lines
8.5 KiB
Python
from templating import renderers
|
|
from routing import lookup_controller
|
|
|
|
from webob import Request, Response, exc
|
|
from threading import local
|
|
from itertools import chain
|
|
from formencode import Invalid
|
|
from paste.recursive import ForwardRequestException
|
|
|
|
|
|
try:
|
|
from json import loads
|
|
except ImportError:
|
|
from simplejson import loads
|
|
|
|
|
|
state = local()
|
|
|
|
|
|
def proxy(key):
|
|
class ObjectProxy(object):
|
|
def __getattr__(self, attr):
|
|
obj = getattr(state, key)
|
|
if attr == 'validation_error':
|
|
return getattr(obj, attr, None)
|
|
return getattr(obj, attr)
|
|
def __setattr__(self, attr, value):
|
|
obj = getattr(state, key)
|
|
return setattr(obj, attr, value)
|
|
return ObjectProxy()
|
|
|
|
|
|
request = proxy('request')
|
|
context = proxy('request.context')
|
|
response = proxy('response')
|
|
|
|
|
|
def override_template(template):
|
|
request.override_template = template
|
|
|
|
|
|
def redirect(location, internal=False):
|
|
if internal:
|
|
raise ForwardRequestException(location)
|
|
raise exc.HTTPFound(location=location)
|
|
|
|
|
|
def error_for(field):
|
|
if request.validation_error is None: return ''
|
|
return request.validation_error.error_dict.get(field, '')
|
|
|
|
|
|
class Pecan(object):
|
|
def __init__(self, root,
|
|
renderers = renderers,
|
|
default_renderer = 'kajiki',
|
|
template_path = 'templates',
|
|
hooks = []):
|
|
|
|
self.root = root
|
|
self.renderers = renderers
|
|
self.default_renderer = default_renderer
|
|
self.hooks = hooks
|
|
self.template_path = template_path
|
|
|
|
def get_content_type(self, format):
|
|
return {
|
|
'html' : 'text/html',
|
|
'xhtml' : 'text/html',
|
|
'json' : 'application/json'
|
|
}.get(format, 'text/html')
|
|
|
|
def route(self, node, path):
|
|
path = path.split('/')[1:]
|
|
node, remainder = lookup_controller(node, path)
|
|
return node, remainder
|
|
|
|
def handle_security(self, controller):
|
|
if controller.pecan.get('secured', False):
|
|
if not controller.pecan['check_permissions']():
|
|
raise exc.HTTPUnauthorized
|
|
|
|
def determine_hooks(self, controller):
|
|
return list(
|
|
sorted(
|
|
chain(controller.pecan.get('hooks', []), self.hooks),
|
|
lambda x,y: cmp(x.priority, y.priority)
|
|
)
|
|
)
|
|
|
|
def handle_hooks(self, hook_type, *args):
|
|
if hook_type == 'before':
|
|
hooks = state.hooks
|
|
else:
|
|
hooks = reversed(state.hooks)
|
|
|
|
for hook in hooks:
|
|
getattr(hook, hook_type)(*args)
|
|
|
|
def get_params(self, all_params, remainder, argspec, im_self):
|
|
valid_params = dict()
|
|
positional_params = []
|
|
|
|
if im_self is not None:
|
|
positional_params.append(im_self)
|
|
|
|
# handle params that are POST or GET variables first
|
|
for param_name, param_value in all_params.iteritems():
|
|
if param_name in argspec[0]:
|
|
valid_params[param_name] = param_value
|
|
|
|
# handle positional arguments
|
|
used = set()
|
|
for i, value in enumerate(remainder):
|
|
if len(argspec.args) > (i+1):
|
|
if valid_params.get(argspec.args[i+1]) is None:
|
|
used.add(i)
|
|
valid_params[argspec.args[i+1]] = value
|
|
|
|
# handle unconsumed positional arguments
|
|
if len(used) < len(remainder) and argspec.varargs is not None:
|
|
for i, value in enumerate(remainder):
|
|
if i not in used:
|
|
positional_params.append(value)
|
|
|
|
return valid_params, positional_params
|
|
|
|
def validate(self, schema, params=None, json=False):
|
|
to_validate = params
|
|
if json:
|
|
to_validate = loads(request.body)
|
|
return schema.to_python(to_validate)
|
|
|
|
def handle_request(self):
|
|
# lookup the controller, respecting content-type as requested
|
|
# by the file extension on the URI
|
|
path = state.request.path
|
|
content_type = None
|
|
if '.' in path.split('/')[-1]:
|
|
path, format = path.split('.')
|
|
content_type = self.get_content_type(format)
|
|
controller, remainder = self.route(self.root, path)
|
|
|
|
if controller.pecan.get('generic_handler'):
|
|
raise exc.HTTPNotFound
|
|
|
|
# handle generic controllers
|
|
im_self = None
|
|
if controller.pecan.get('generic'):
|
|
im_self = controller.im_self
|
|
handlers = controller.pecan['generic_handlers']
|
|
controller = handlers.get(request.method, handlers['DEFAULT'])
|
|
|
|
# determine content type
|
|
if content_type is None:
|
|
content_type = controller.pecan.get('content_type', 'text/html')
|
|
|
|
# handle security
|
|
self.handle_security(controller)
|
|
|
|
# get a sorted list of hooks, by priority
|
|
state.hooks = self.determine_hooks(controller)
|
|
|
|
# handle "before" hooks
|
|
self.handle_hooks('before', state)
|
|
|
|
# fetch and validate any parameters
|
|
params, positional_params = self.get_params(
|
|
dict(state.request.str_params),
|
|
remainder,
|
|
controller.pecan['argspec'],
|
|
im_self
|
|
)
|
|
if 'schema' in controller.pecan:
|
|
request.validation_error = None
|
|
try:
|
|
params = self.validate(
|
|
controller.pecan['schema'],
|
|
json = controller.pecan['validate_json'],
|
|
params = params
|
|
)
|
|
except Invalid, e:
|
|
request.validation_error = e
|
|
if controller.pecan['error_handler'] is not None:
|
|
redirect(controller.pecan['error_handler'], internal=True)
|
|
if controller.pecan['validate_json']: params = dict(data=params)
|
|
|
|
# get the result from the controller
|
|
result = controller(*positional_params, **params)
|
|
|
|
# pull the template out based upon content type and handle overrides
|
|
template = controller.pecan.get('content_types', {}).get(content_type)
|
|
template = getattr(request, 'override_template', template)
|
|
|
|
# if there is a template, render it
|
|
if template:
|
|
renderer = self.renderers.get(self.default_renderer, self.template_path)
|
|
if template == 'json':
|
|
renderer = self.renderers.get('json', self.template_path)
|
|
else:
|
|
result['error_for'] = error_for
|
|
|
|
if ':' in template:
|
|
renderer = self.renderers.get(template.split(':')[0], self.template_path)
|
|
template = template.split(':')[1]
|
|
result = renderer.render(template, result)
|
|
content_type = renderer.content_type
|
|
|
|
# set the body content
|
|
if isinstance(result, unicode):
|
|
state.response.unicode_body = result
|
|
else:
|
|
state.response.body = result
|
|
|
|
# set the content type
|
|
if content_type:
|
|
state.response.content_type = content_type
|
|
|
|
def __call__(self, environ, start_response):
|
|
# create the request and response object
|
|
state.request = Request(environ)
|
|
state.response = Response()
|
|
state.hooks = []
|
|
state.app = self
|
|
|
|
# handle the request
|
|
try:
|
|
# add context to the request
|
|
state.request.context = {}
|
|
|
|
self.handle_request()
|
|
except Exception, e:
|
|
# if this is an HTTP Exception, set it as the response
|
|
if isinstance(e, exc.HTTPException):
|
|
state.response = e
|
|
|
|
# if this is not an internal redirect, run error hooks
|
|
if not isinstance(e, ForwardRequestException):
|
|
self.handle_hooks('on_error', state, e)
|
|
|
|
if not isinstance(e, exc.HTTPException):
|
|
raise
|
|
finally:
|
|
# handle "after" hooks
|
|
self.handle_hooks('after', state)
|
|
|
|
# get the response
|
|
try:
|
|
return state.response(environ, start_response)
|
|
finally:
|
|
# clean up state
|
|
del state.request
|
|
del state.response
|
|
del state.hooks
|