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.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
|
||||||
@@ -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()
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
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(
|
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'
|
||||||
|
|||||||
@@ -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!'
|
||||||
Reference in New Issue
Block a user