Vast improvements to validation, based on suggestions by Rick Copeland.

Error handlers are now internal redirects, rather than browser
redirects.

Added error handler middleware. This needs some configuration improvements.

Made the project template much better, with some CSS, and a Kajiki-based
layout.
This commit is contained in:
Jonathan LaCour
2010-10-12 11:46:31 -04:00
parent b7e444628e
commit 33719028fb
11 changed files with 150 additions and 33 deletions

View File

@@ -1,7 +1,9 @@
from paste.urlparser import StaticURLParser from paste.urlparser import StaticURLParser
from paste.cascade import Cascade from paste.cascade import Cascade
from weberror.errormiddleware import ErrorMiddleware
from paste.recursive import RecursiveMiddleware
from pecan import Pecan, request, response, override_template, redirect from pecan import Pecan, request, response, override_template, redirect, error_for
from decorators import expose from decorators import expose
__all__ = [ __all__ = [
@@ -9,8 +11,10 @@ __all__ = [
] ]
def make_app(root, static_root=None, **kw): def make_app(root, static_root=None, debug=False, errorcfg={}, **kw):
app = Pecan(root, **kw) app = Pecan(root, **kw)
app = RecursiveMiddleware(app)
app = ErrorMiddleware(app, debug=debug, **errorcfg)
if static_root: if static_root:
app = Cascade([StaticURLParser(static_root), app]) app = Cascade([StaticURLParser(static_root), app])
return app return app

View File

@@ -5,6 +5,7 @@ from webob import Request, Response, exc
from threading import local from threading import local
from itertools import chain from itertools import chain
from formencode import Invalid from formencode import Invalid
from paste.recursive import ForwardRequestException
try: try:
from json import loads from json import loads
@@ -19,6 +20,8 @@ def proxy(key):
class ObjectProxy(object): class ObjectProxy(object):
def __getattr__(self, attr): def __getattr__(self, attr):
obj = getattr(state, key) obj = getattr(state, key)
if attr == 'validation_error':
return getattr(obj, attr, None)
return getattr(obj, attr) return getattr(obj, attr)
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
obj = getattr(state, key) obj = getattr(state, key)
@@ -37,6 +40,10 @@ def override_template(template):
def redirect(location): def redirect(location):
raise exc.HTTPFound(location=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): class Pecan(object):
def __init__(self, root, def __init__(self, root,
@@ -122,6 +129,7 @@ class Pecan(object):
controller.pecan['argspec'] controller.pecan['argspec']
) )
if 'schema' in controller.pecan: if 'schema' in controller.pecan:
request.validation_error = None
try: try:
params = self.validate( params = self.validate(
controller.pecan['schema'], controller.pecan['schema'],
@@ -131,8 +139,7 @@ class Pecan(object):
except Invalid, e: except Invalid, e:
request.validation_error = e request.validation_error = e
if controller.pecan['error_handler'] is not None: if controller.pecan['error_handler'] is not None:
state.validation_error = e raise ForwardRequestException(controller.pecan['error_handler'])
redirect(controller.pecan['error_handler'])
if controller.pecan['validate_json']: params = dict(data=params) if controller.pecan['validate_json']: params = dict(data=params)
# get the result from the controller # get the result from the controller
@@ -147,7 +154,10 @@ class Pecan(object):
renderer = self.renderers.get(self.default_renderer, self.template_path) renderer = self.renderers.get(self.default_renderer, self.template_path)
if template == 'json': if template == 'json':
renderer = self.renderers.get('json', self.template_path) renderer = self.renderers.get('json', self.template_path)
elif ':' in template: else:
result['error_for'] = error_for
if ':' in template:
renderer = self.renderers.get(template.split(':')[0], self.template_path) renderer = self.renderers.get(template.split(':')[0], self.template_path)
template = template.split(':')[1] template = template.split(':')[1]
result = renderer.render(template, result) result = renderer.render(template, result)
@@ -168,12 +178,7 @@ class Pecan(object):
state.request = Request(environ) state.request = Request(environ)
state.response = Response() state.response = Response()
state.hooks = [] state.hooks = []
# handle validation errors from redirects
if hasattr(state, 'validation_error'):
state.request.validation_error = state.validation_error
del state.validation_error
# handle the request # handle the request
try: try:
self.handle_request() self.handle_request()

View File

@@ -21,7 +21,8 @@ class PyTest(Command):
# determine requirements # determine requirements
# #
requirements = [ requirements = [
"WebOb >= 0.9.8", "WebOb >= 1.0.0",
"WebCore >= 1.0.0",
"simplegeneric >= 0.7", "simplegeneric >= 0.7",
"Genshi >= 0.6", "Genshi >= 0.6",
"Kajiki >= 0.2.2", "Kajiki >= 0.2.2",

View File

@@ -1,6 +1,17 @@
from pecan import expose from pecan import expose, request
from formencode import Schema, validators as v
class SampleForm(Schema):
name = v.String(not_empty=True)
age = v.Int(not_empty=True)
class RootController(object): class RootController(object):
@expose('kajiki:index.html') @expose('kajiki:index.html')
def index(self, name='World'): def index(self, name='', age=''):
return dict(name=name) return dict(errors=request.validation_error, name=name, age=age)
@expose('kajiki:success.html', schema=SampleForm(), error_handler='index')
def handle_form(self, name, age):
return dict(name=name, age=age)

View File

@@ -1,8 +1,44 @@
<html> <html py:extends="layout.html">
<head> <head>
<title>Hello, ${name}</title> <title py:block="title">Welcome to Pecan!</title>
</head> </head>
<body> <body py:block="body">
<h1>Hello, ${name}!</h1> <header>
<h1>Welcome to Pecan</h1>
</header>
<p>This is the default Pecan project, which includes:</p>
<ul>
<li>The ability to serve static files.</li>
<li>Templating using the <a href="http://kajiki.pythonisito.com/">Kajiki template engine</a>.</li>
<li>Built-in error management using <a href="http://pypi.python.org/pypi/WebError">WebError</a>.</li>
</ul>
<p>
Note that most functionality is enabled/disabled in your <code>start.py</code>,
which can be edited to suit your needs, and to add additional WSGI middleware.
</p>
<p>
To get an idea of how to develop applications with Pecan,
here is a simple form:
</p>
<form method="POST" action="/handle_form" class="${'invalid' if errors else ''}">
<table>
<tr>
<td><label for="name">What is your name</label></td>
<td><input name="name" value="${name}" class="${'invalid' if error_for('name') else ''}" /></td>
<td py:if="errors">${error_for('name')}</td>
</tr>
<tr>
<td><label for="age">How old are you?</label></td>
<td><input name="age" value="${age}" class="${'invalid' if error_for('age') else ''}" /></td>
<td py:if="errors">${error_for('age')}</td>
</tr>
</table>
<input type="submit" />
</form>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,13 @@
<html>
<head>
<title py:block="title">Default Title</title>
<py:block name="style">
<link rel="stylesheet" type="text/css" media="screen" href="/css/style.css" />
</py:block>
<py:block name="javascript" py:strip="True">
<script language="text/javascript" src="/javascript/shared.js" />
</py:block>
</head>
<body py:block="body">
</body>
</html>

View File

@@ -0,0 +1,11 @@
<html py:extends="layout.html">
<head>
<title py:block="title">Success!</title>
</head>
<body py:block="body">
<header>
<h1>Success!</h1>
</header>
<p>Your form submission was successful! Thanks, ${name}!</p>
</body>
</html>

View File

@@ -0,0 +1,39 @@
body {
background: #311F00;
color: white;
font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif;
font-size: 1.25em;
padding: 1em 2em;
}
a {
color: #FAFF78;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
form {
margin: 0 1em;
padding: 1em;
border: 5px transparent;
}
form.invalid {
border: 5px solid FAFF78;
}
input.invalid {
background: #FAFF78;
}
header {
text-align: center;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Futura-CondensedExtraBold', 'Futura', 'Helvetica', sans-serif;
text-transform: uppercase;
}

View File

@@ -5,7 +5,8 @@ if __name__ == '__main__':
app = make_app( app = make_app(
RootController(), RootController(),
static_root='public', static_root='public',
template_path='${egg}/templates' template_path='${egg}/templates',
debug=True
) )
print 'Serving on http://0.0.0.0:8080' print 'Serving on http://0.0.0.0:8080'

View File

@@ -1,4 +1,4 @@
from pecan import Pecan, expose, request, response from pecan import make_app, expose, request, response
from webtest import TestApp from webtest import TestApp
from formencode import validators, Schema from formencode import validators, Schema
@@ -45,7 +45,7 @@ class TestValidation(object):
return 'Success!' return 'Success!'
# test form submissions # test form submissions
app = TestApp(Pecan(RootController())) app = TestApp(make_app(RootController()))
r = app.post('/', dict( r = app.post('/', dict(
first_name='Jonathan', first_name='Jonathan',
last_name='LaCour', last_name='LaCour',
@@ -85,6 +85,11 @@ class TestValidation(object):
] ]
class RootController(object): class RootController(object):
@expose()
def errors(self, *args, **kwargs):
assert request.validation_error is not None
return 'There was an error!'
@expose(schema=RegistrationSchema()) @expose(schema=RegistrationSchema())
def index(self, first_name, def index(self, first_name,
last_name, last_name,
@@ -116,15 +121,10 @@ class TestValidation(object):
def json_with_handler(self, data): def json_with_handler(self, data):
assert request.validation_error is not None assert request.validation_error is not None
return 'Success!' return 'Success!'
@expose()
def errors(self, *args, **kwargs):
assert request.validation_error is not None
return 'There was an error!'
# test without error handler # test without error handler
app = TestApp(Pecan(RootController())) app = TestApp(make_app(RootController()))
r = app.post('/', dict( r = app.post('/', dict(
first_name='Jonathan', first_name='Jonathan',
last_name='LaCour', last_name='LaCour',
@@ -138,7 +138,7 @@ class TestValidation(object):
assert r.body == 'Success!' assert r.body == 'Success!'
# test with error handler # test with error handler
app = TestApp(Pecan(RootController())) app = TestApp(make_app(RootController()))
r = app.post('/with_handler', dict( r = app.post('/with_handler', dict(
first_name='Jonathan', first_name='Jonathan',
last_name='LaCour', last_name='LaCour',
@@ -148,8 +148,6 @@ class TestValidation(object):
password_confirm='654321', password_confirm='654321',
age='31' age='31'
)) ))
assert r.status_int == 302
r = r.follow()
assert r.status_int == 200 assert r.status_int == 200
assert r.body == 'There was an error!' assert r.body == 'There was an error!'
@@ -176,7 +174,5 @@ class TestValidation(object):
password_confirm='654321', password_confirm='654321',
age='31' age='31'
)), [('content-type', 'application/json')]) )), [('content-type', 'application/json')])
assert r.status_int == 302
r = r.follow()
assert r.status_int == 200 assert r.status_int == 200
assert r.body == 'There was an error!' assert r.body == 'There was an error!'