Adding validation support.
This commit is contained in:
@@ -1,18 +1,37 @@
|
|||||||
from inspect import getargspec
|
from inspect import getargspec
|
||||||
|
|
||||||
def expose(template=None, content_type='text/html'):
|
def _cfg(f):
|
||||||
|
if not hasattr(f, 'pecan'): f.pecan = {}
|
||||||
|
return f.pecan
|
||||||
|
|
||||||
|
|
||||||
|
def expose(template = None,
|
||||||
|
content_type = 'text/html',
|
||||||
|
schema = None,
|
||||||
|
json_schema = None,
|
||||||
|
error_handler = None):
|
||||||
|
|
||||||
if template == 'json': content_type = 'application/json'
|
if template == 'json': content_type = 'application/json'
|
||||||
def decorate(f):
|
def decorate(f):
|
||||||
# flag the method as exposed
|
# flag the method as exposed
|
||||||
f.exposed = True
|
f.exposed = True
|
||||||
|
|
||||||
# set a "pecan" attribute, where we will store details
|
# set a "pecan" attribute, where we will store details
|
||||||
if not hasattr(f, 'pecan'): f.pecan = {}
|
cfg = _cfg(f)
|
||||||
f.pecan['content_type'] = content_type
|
cfg['content_type'] = content_type
|
||||||
f.pecan.setdefault('template', []).append(template)
|
cfg.setdefault('template', []).append(template)
|
||||||
f.pecan.setdefault('content_types', {})[content_type] = template
|
cfg.setdefault('content_types', {})[content_type] = template
|
||||||
|
|
||||||
# store the arguments for this controller method
|
# store the arguments for this controller method
|
||||||
f.pecan['argspec'] = getargspec(f)
|
cfg['argspec'] = getargspec(f)
|
||||||
|
|
||||||
|
# store the validator
|
||||||
|
cfg['error_handler'] = error_handler
|
||||||
|
if schema is not None:
|
||||||
|
cfg['schema'] = schema
|
||||||
|
cfg['validate_json'] = False
|
||||||
|
elif json_schema is not None:
|
||||||
|
cfg['schema'] = json_schema
|
||||||
|
cfg['validate_json'] = True
|
||||||
return f
|
return f
|
||||||
return decorate
|
return decorate
|
||||||
@@ -4,6 +4,12 @@ from routing import lookup_controller
|
|||||||
from webob import Request, Response, exc
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
from json import loads
|
||||||
|
except ImportError:
|
||||||
|
from simplejson import loads
|
||||||
|
|
||||||
|
|
||||||
state = local()
|
state = local()
|
||||||
@@ -74,13 +80,19 @@ class Pecan(object):
|
|||||||
for hook in state.hooks:
|
for hook in state.hooks:
|
||||||
getattr(hook, hook_type)(*args)
|
getattr(hook, hook_type)(*args)
|
||||||
|
|
||||||
def get_validated_params(self, all_params, argspec):
|
def get_params(self, all_params, argspec):
|
||||||
valid_params = dict()
|
valid_params = dict()
|
||||||
for param_name, param_value in all_params.iteritems():
|
for param_name, param_value in all_params.iteritems():
|
||||||
if param_name in argspec.args:
|
if param_name in argspec.args:
|
||||||
valid_params[param_name] = param_value
|
valid_params[param_name] = param_value
|
||||||
return valid_params
|
return valid_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):
|
def handle_request(self):
|
||||||
# lookup the controller, respecting content-type as requested
|
# lookup the controller, respecting content-type as requested
|
||||||
# by the file extension on the URI
|
# by the file extension on the URI
|
||||||
@@ -104,11 +116,26 @@ class Pecan(object):
|
|||||||
# handle "before" hooks
|
# handle "before" hooks
|
||||||
self.handle_hooks('before', state)
|
self.handle_hooks('before', state)
|
||||||
|
|
||||||
# get the result from the controller, properly handling wrap hooks
|
# fetch and validate any parameters
|
||||||
params = self.get_validated_params(
|
params = self.get_params(
|
||||||
dict(state.request.str_params),
|
dict(state.request.str_params),
|
||||||
controller.pecan['argspec']
|
controller.pecan['argspec']
|
||||||
)
|
)
|
||||||
|
if 'schema' in controller.pecan:
|
||||||
|
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:
|
||||||
|
state.validation_error = e
|
||||||
|
redirect(controller.pecan['error_handler'])
|
||||||
|
if controller.pecan['validate_json']: params = dict(data=params)
|
||||||
|
|
||||||
|
# get the result from the controller
|
||||||
result = controller(**params)
|
result = controller(**params)
|
||||||
|
|
||||||
# pull the template out based upon content type and handle overrides
|
# pull the template out based upon content type and handle overrides
|
||||||
@@ -142,6 +169,11 @@ class Pecan(object):
|
|||||||
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
@@ -29,7 +29,8 @@ requirements = [
|
|||||||
"py >= 1.3.4",
|
"py >= 1.3.4",
|
||||||
"WebTest >= 1.2.2",
|
"WebTest >= 1.2.2",
|
||||||
"Paste >= 1.7.5.1",
|
"Paste >= 1.7.5.1",
|
||||||
"PasteScript >= 1.7.3"
|
"PasteScript >= 1.7.3",
|
||||||
|
"formencode >= 1.2.2"
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ class TestStatic(object):
|
|||||||
assert response.body == 'Hello, World!'
|
assert response.body == 'Hello, World!'
|
||||||
|
|
||||||
# get a static resource
|
# get a static resource
|
||||||
response = app.get('/test.txt')
|
response = app.get('/text.txt')
|
||||||
assert response.status_int == 200
|
assert response.status_int == 200
|
||||||
assert response.body == open('tests/static/test.txt', 'rb').read()
|
assert response.body == open('tests/static/text.txt', 'rb').read()
|
||||||
182
tests/test_validation.py
Normal file
182
tests/test_validation.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
from pecan import Pecan, expose, request, response
|
||||||
|
from webtest import TestApp
|
||||||
|
|
||||||
|
from formencode import validators, Schema
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from json import dumps
|
||||||
|
except ImportError:
|
||||||
|
from simplejson import dumps
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidation(object):
|
||||||
|
|
||||||
|
def test_simple_validation(self):
|
||||||
|
class RegistrationSchema(Schema):
|
||||||
|
first_name = validators.String(not_empty=True)
|
||||||
|
last_name = validators.String(not_empty=True)
|
||||||
|
email = validators.Email()
|
||||||
|
username = validators.PlainText()
|
||||||
|
password = validators.String()
|
||||||
|
password_confirm = validators.String()
|
||||||
|
age = validators.Int()
|
||||||
|
chained_validators = [
|
||||||
|
validators.FieldsMatch('password', 'password_confirm')
|
||||||
|
]
|
||||||
|
|
||||||
|
class RootController(object):
|
||||||
|
@expose(schema=RegistrationSchema())
|
||||||
|
def index(self, first_name,
|
||||||
|
last_name,
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
password_confirm,
|
||||||
|
age):
|
||||||
|
assert age == 31
|
||||||
|
assert isinstance(age, int)
|
||||||
|
return 'Success!'
|
||||||
|
|
||||||
|
@expose(json_schema=RegistrationSchema())
|
||||||
|
def json(self, data):
|
||||||
|
assert data['age'] == 31
|
||||||
|
assert isinstance(data['age'], int)
|
||||||
|
return 'Success!'
|
||||||
|
|
||||||
|
# test form submissions
|
||||||
|
app = TestApp(Pecan(RootController()))
|
||||||
|
r = app.post('/', dict(
|
||||||
|
first_name='Jonathan',
|
||||||
|
last_name='LaCour',
|
||||||
|
email='jonathan@cleverdevil.org',
|
||||||
|
username='jlacour',
|
||||||
|
password='123456',
|
||||||
|
password_confirm='123456',
|
||||||
|
age='31'
|
||||||
|
))
|
||||||
|
assert r.status_int == 200
|
||||||
|
assert r.body == 'Success!'
|
||||||
|
|
||||||
|
# test JSON submissions
|
||||||
|
r = app.post('/json', dumps(dict(
|
||||||
|
first_name='Jonathan',
|
||||||
|
last_name='LaCour',
|
||||||
|
email='jonathan@cleverdevil.org',
|
||||||
|
username='jlacour',
|
||||||
|
password='123456',
|
||||||
|
password_confirm='123456',
|
||||||
|
age='31'
|
||||||
|
)), [('content-type', 'application/json')])
|
||||||
|
assert r.status_int == 200
|
||||||
|
assert r.body == 'Success!'
|
||||||
|
|
||||||
|
def test_simple_failure(self):
|
||||||
|
class RegistrationSchema(Schema):
|
||||||
|
first_name = validators.String(not_empty=True)
|
||||||
|
last_name = validators.String(not_empty=True)
|
||||||
|
email = validators.Email()
|
||||||
|
username = validators.PlainText()
|
||||||
|
password = validators.String()
|
||||||
|
password_confirm = validators.String()
|
||||||
|
age = validators.Int()
|
||||||
|
chained_validators = [
|
||||||
|
validators.FieldsMatch('password', 'password_confirm')
|
||||||
|
]
|
||||||
|
|
||||||
|
class RootController(object):
|
||||||
|
@expose(schema=RegistrationSchema())
|
||||||
|
def index(self, first_name,
|
||||||
|
last_name,
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
password_confirm,
|
||||||
|
age):
|
||||||
|
assert request.validation_error is not None
|
||||||
|
return 'Success!'
|
||||||
|
|
||||||
|
@expose(schema=RegistrationSchema(), error_handler='/errors')
|
||||||
|
def with_handler(self, first_name,
|
||||||
|
last_name,
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
password_confirm,
|
||||||
|
age):
|
||||||
|
assert request.validation_error is not None
|
||||||
|
return 'Success!'
|
||||||
|
|
||||||
|
@expose(json_schema=RegistrationSchema())
|
||||||
|
def json(self, data):
|
||||||
|
assert request.validation_error is not None
|
||||||
|
return 'Success!'
|
||||||
|
|
||||||
|
@expose(json_schema=RegistrationSchema(), error_handler='/errors')
|
||||||
|
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()))
|
||||||
|
r = app.post('/', dict(
|
||||||
|
first_name='Jonathan',
|
||||||
|
last_name='LaCour',
|
||||||
|
email='jonathan@cleverdevil.org',
|
||||||
|
username='jlacour',
|
||||||
|
password='123456',
|
||||||
|
password_confirm='654321',
|
||||||
|
age='31'
|
||||||
|
))
|
||||||
|
assert r.status_int == 200
|
||||||
|
assert r.body == 'Success!'
|
||||||
|
|
||||||
|
# test with error handler
|
||||||
|
app = TestApp(Pecan(RootController()))
|
||||||
|
r = app.post('/with_handler', dict(
|
||||||
|
first_name='Jonathan',
|
||||||
|
last_name='LaCour',
|
||||||
|
email='jonathan@cleverdevil.org',
|
||||||
|
username='jlacour',
|
||||||
|
password='123456',
|
||||||
|
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!'
|
||||||
|
|
||||||
|
# test JSON without error handler
|
||||||
|
r = app.post('/json', dumps(dict(
|
||||||
|
first_name='Jonathan',
|
||||||
|
last_name='LaCour',
|
||||||
|
email='jonathan@cleverdevil.org',
|
||||||
|
username='jlacour',
|
||||||
|
password='123456',
|
||||||
|
password_confirm='654321',
|
||||||
|
age='31'
|
||||||
|
)), [('content-type', 'application/json')])
|
||||||
|
assert r.status_int == 200
|
||||||
|
assert r.body == 'Success!'
|
||||||
|
|
||||||
|
# test JSON with error handler
|
||||||
|
r = app.post('/json_with_handler', dumps(dict(
|
||||||
|
first_name='Jonathan',
|
||||||
|
last_name='LaCour',
|
||||||
|
email='jonathan@cleverdevil.org',
|
||||||
|
username='jlacour',
|
||||||
|
password='123456',
|
||||||
|
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