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:
@@ -1,7 +1,9 @@
|
||||
from paste.urlparser import StaticURLParser
|
||||
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
|
||||
|
||||
__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 = RecursiveMiddleware(app)
|
||||
app = ErrorMiddleware(app, debug=debug, **errorcfg)
|
||||
if static_root:
|
||||
app = Cascade([StaticURLParser(static_root), app])
|
||||
return app
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -19,6 +20,8 @@ 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)
|
||||
@@ -37,6 +40,10 @@ def override_template(template):
|
||||
def redirect(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,
|
||||
@@ -122,6 +129,7 @@ class Pecan(object):
|
||||
controller.pecan['argspec']
|
||||
)
|
||||
if 'schema' in controller.pecan:
|
||||
request.validation_error = None
|
||||
try:
|
||||
params = self.validate(
|
||||
controller.pecan['schema'],
|
||||
@@ -131,8 +139,7 @@ class Pecan(object):
|
||||
except Invalid, e:
|
||||
request.validation_error = e
|
||||
if controller.pecan['error_handler'] is not None:
|
||||
state.validation_error = e
|
||||
redirect(controller.pecan['error_handler'])
|
||||
raise ForwardRequestException(controller.pecan['error_handler'])
|
||||
if controller.pecan['validate_json']: params = dict(data=params)
|
||||
|
||||
# get the result from the controller
|
||||
@@ -147,7 +154,10 @@ class Pecan(object):
|
||||
renderer = self.renderers.get(self.default_renderer, self.template_path)
|
||||
if template == 'json':
|
||||
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)
|
||||
template = template.split(':')[1]
|
||||
result = renderer.render(template, result)
|
||||
@@ -168,12 +178,7 @@ class Pecan(object):
|
||||
state.request = Request(environ)
|
||||
state.response = Response()
|
||||
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
|
||||
try:
|
||||
self.handle_request()
|
||||
|
||||
3
setup.py
3
setup.py
@@ -21,7 +21,8 @@ class PyTest(Command):
|
||||
# determine requirements
|
||||
#
|
||||
requirements = [
|
||||
"WebOb >= 0.9.8",
|
||||
"WebOb >= 1.0.0",
|
||||
"WebCore >= 1.0.0",
|
||||
"simplegeneric >= 0.7",
|
||||
"Genshi >= 0.6",
|
||||
"Kajiki >= 0.2.2",
|
||||
|
||||
@@ -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):
|
||||
@expose('kajiki:index.html')
|
||||
def index(self, name='World'):
|
||||
return dict(name=name)
|
||||
def index(self, name='', age=''):
|
||||
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)
|
||||
|
||||
@@ -1,8 +1,44 @@
|
||||
<html>
|
||||
<html py:extends="layout.html">
|
||||
<head>
|
||||
<title>Hello, ${name}</title>
|
||||
<title py:block="title">Welcome to Pecan!</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello, ${name}!</h1>
|
||||
<body py:block="body">
|
||||
<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>
|
||||
</html>
|
||||
13
templates/project/+egg+/templates/layout.html
Normal file
13
templates/project/+egg+/templates/layout.html
Normal 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>
|
||||
11
templates/project/+egg+/templates/success.html
Normal file
11
templates/project/+egg+/templates/success.html
Normal 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>
|
||||
39
templates/project/public/css/style.css
Normal file
39
templates/project/public/css/style.css
Normal 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;
|
||||
}
|
||||
0
templates/project/public/javascript/shared.js
Normal file
0
templates/project/public/javascript/shared.js
Normal file
@@ -5,7 +5,8 @@ if __name__ == '__main__':
|
||||
app = make_app(
|
||||
RootController(),
|
||||
static_root='public',
|
||||
template_path='${egg}/templates'
|
||||
template_path='${egg}/templates',
|
||||
debug=True
|
||||
)
|
||||
|
||||
print 'Serving on http://0.0.0.0:8080'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pecan import Pecan, expose, request, response
|
||||
from pecan import make_app, expose, request, response
|
||||
from webtest import TestApp
|
||||
|
||||
from formencode import validators, Schema
|
||||
@@ -45,7 +45,7 @@ class TestValidation(object):
|
||||
return 'Success!'
|
||||
|
||||
# test form submissions
|
||||
app = TestApp(Pecan(RootController()))
|
||||
app = TestApp(make_app(RootController()))
|
||||
r = app.post('/', dict(
|
||||
first_name='Jonathan',
|
||||
last_name='LaCour',
|
||||
@@ -85,6 +85,11 @@ class TestValidation(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())
|
||||
def index(self, first_name,
|
||||
last_name,
|
||||
@@ -116,15 +121,10 @@ class TestValidation(object):
|
||||
def json_with_handler(self, data):
|
||||
assert request.validation_error is not None
|
||||
return 'Success!'
|
||||
|
||||
@expose()
|
||||
def errors(self, *args, **kwargs):
|
||||
assert request.validation_error is not None
|
||||
return 'There was an error!'
|
||||
|
||||
|
||||
# test without error handler
|
||||
app = TestApp(Pecan(RootController()))
|
||||
app = TestApp(make_app(RootController()))
|
||||
r = app.post('/', dict(
|
||||
first_name='Jonathan',
|
||||
last_name='LaCour',
|
||||
@@ -138,7 +138,7 @@ class TestValidation(object):
|
||||
assert r.body == 'Success!'
|
||||
|
||||
# test with error handler
|
||||
app = TestApp(Pecan(RootController()))
|
||||
app = TestApp(make_app(RootController()))
|
||||
r = app.post('/with_handler', dict(
|
||||
first_name='Jonathan',
|
||||
last_name='LaCour',
|
||||
@@ -148,8 +148,6 @@ class TestValidation(object):
|
||||
password_confirm='654321',
|
||||
age='31'
|
||||
))
|
||||
assert r.status_int == 302
|
||||
r = r.follow()
|
||||
assert r.status_int == 200
|
||||
assert r.body == 'There was an error!'
|
||||
|
||||
@@ -176,7 +174,5 @@ class TestValidation(object):
|
||||
password_confirm='654321',
|
||||
age='31'
|
||||
)), [('content-type', 'application/json')])
|
||||
assert r.status_int == 302
|
||||
r = r.follow()
|
||||
assert r.status_int == 200
|
||||
assert r.body == 'There was an error!'
|
||||
Reference in New Issue
Block a user