diff --git a/pecan/core.py b/pecan/core.py index ff34ee9..3007e9f 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -7,6 +7,7 @@ from webob import Request, Response, exc from threading import local from itertools import chain from formencode import htmlfill, Invalid, variabledecode +from formencode.schema import merge_dicts from paste.recursive import ForwardRequestException try: @@ -58,12 +59,13 @@ def redirect(location, internal=False, code=None, headers={}): def error_for(field): - if request.validation_errors is None: return '' + if not request.validation_errors: + return '' return request.validation_errors.get(field, '') class ValidationException(ForwardRequestException): - def __init__(self, location=None): + def __init__(self, location=None, errors={}): if hasattr(state, 'controller'): cfg = _cfg(state.controller) else: @@ -72,6 +74,7 @@ class ValidationException(ForwardRequestException): location = cfg['error_handler'] if callable(location): location = location() + merge_dicts(request.validation_errors, errors) request.environ['pecan.params'] = dict(request.str_params) request.environ['pecan.validation_errors'] = request.validation_errors if cfg.get('htmlfill') is not None: @@ -178,7 +181,7 @@ class Pecan(object): def validate(self, schema, params, json=False, error_handler=None, htmlfill=None, variable_decode=None): - request.validation_errors = None + request.validation_errors = {} try: to_validate = params if json: diff --git a/tests/templates/form_name_invalid_custom.html b/tests/templates/form_name_invalid_custom.html new file mode 100644 index 0000000..cbb631f --- /dev/null +++ b/tests/templates/form_name_invalid_custom.html @@ -0,0 +1,3 @@ + +Names must be unique
+ \ No newline at end of file diff --git a/tests/test_base.py b/tests/test_base.py index fa7ddd8..b05bfd9 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -3,7 +3,7 @@ from paste.recursive import ForwardRequestException from unittest import TestCase from webtest import TestApp -from pecan import Pecan, expose, request, response, redirect, abort, make_app, ValidationException +from pecan import Pecan, expose, request, response, redirect, abort, make_app from pecan.templating import _builtin_renderers as builtin_renderers import os @@ -543,25 +543,6 @@ class TestBase(TestCase): app = make_app(RootController(), wrap_app=wrap, debug=True) assert len(wrapped_apps) == 1 - - def test_validation_exception(self): - class SubController(object): - @expose() - def _route(self, *args): - raise ValidationException('/success') - - class RootController(object): - - sub = SubController() - - @expose() - def success(self): - return 'Success!' - - app = TestApp(make_app(RootController())) - r = app.get('/sub') - assert r.status_int == 200 - assert r.body == 'Success!' class TestEngines(object): diff --git a/tests/test_hooks.py b/tests/test_hooks.py index c97c348..232d2a3 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -696,7 +696,7 @@ class TestHooks(object): password_confirm, age): run_hook.append('inside') - return str(request.validation_errors is not None) + return str(len(request.validation_errors) > 0) @expose(schema=RegistrationSchema(), error_handler='/errors') def with_handler(self, first_name, @@ -707,7 +707,7 @@ class TestHooks(object): password_confirm, age): run_hook.append('inside') - return str(request.validation_errors is not None) + return str(len(request.validation_errors) > 0) # test that the hooks get properly run with no validation errors app = TestApp(make_app(RootController(), hooks=[SimpleHook()])) diff --git a/tests/test_validation.py b/tests/test_validation.py index 156d175..d53839f 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -3,7 +3,7 @@ from webtest import TestApp import os.path -from pecan import make_app, expose, request, response, redirect +from pecan import make_app, expose, request, response, redirect, ValidationException from pecan.templating import _builtin_renderers as builtin_renderers try: @@ -95,7 +95,7 @@ class TestValidation(object): @expose() def errors(self, *args, **kwargs): - assert request.validation_errors is not None + assert len(request.validation_errors) > 0 return 'There was an error!' @expose(schema=RegistrationSchema()) @@ -106,7 +106,7 @@ class TestValidation(object): password, password_confirm, age): - assert request.validation_errors is not None + assert len(request.validation_errors) > 0 return 'Success!' @expose(schema=RegistrationSchema(), error_handler='/errors') @@ -117,17 +117,17 @@ class TestValidation(object): password, password_confirm, age): - assert request.validation_errors is not None + assert len(request.validation_errors) > 0 return 'Success!' @expose(json_schema=RegistrationSchema()) def json(self, data): - assert request.validation_errors is not None + assert len(request.validation_errors) > 0 return 'Success!' @expose(json_schema=RegistrationSchema(), error_handler='/errors') def json_with_handler(self, data): - assert request.validation_errors is not None + assert len(request.validation_errors) > 0 return 'Success!' @@ -198,7 +198,7 @@ class TestValidation(object): @expose(schema=ColorSchema(), variable_decode=True) def index(self, **kwargs): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -207,7 +207,7 @@ class TestValidation(object): error_handler='/errors', variable_decode=True) def with_handler(self, **kwargs): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -215,7 +215,7 @@ class TestValidation(object): @expose(json_schema=ColorSchema(), variable_decode=True) def json(self, data): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -224,7 +224,7 @@ class TestValidation(object): error_handler='/errors', variable_decode=True) def json_with_handler(self, data): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -232,7 +232,7 @@ class TestValidation(object): @expose(schema=ColorSchema(), variable_decode=dict()) def custom(self, **kwargs): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -241,7 +241,7 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict()) def custom_with_handler(self, **kwargs): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -249,7 +249,7 @@ class TestValidation(object): @expose(json_schema=ColorSchema(), variable_decode=dict()) def custom_json(self, data): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -258,7 +258,7 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict()) def custom_json_with_handler(self, data): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -266,7 +266,7 @@ class TestValidation(object): @expose(schema=ColorSchema(), variable_decode=dict(dict_char='-', list_char='.')) def alternate(self, **kwargs): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -275,7 +275,7 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict(dict_char='-', list_char='.')) def alternate_with_handler(self, **kwargs): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -283,7 +283,7 @@ class TestValidation(object): @expose(json_schema=ColorSchema(), variable_decode=dict(dict_char='-', list_char='.')) def alternate_json(self, data): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -292,7 +292,7 @@ class TestValidation(object): error_handler='/errors', variable_decode=dict(dict_char='-', list_char='.')) def alternate_json_with_handler(self, data): - if request.validation_errors is not None: + if request.validation_errors: return ', '.join(request.validation_errors.keys()) else: return 'Success!' @@ -627,6 +627,9 @@ class TestValidation(object): def test_error_for(self): + if 'mako' not in builtin_renderers: + return + class ColorSchema(Schema): colors = ForEach(validators.String(not_empty=True)) @@ -662,6 +665,14 @@ class TestValidation(object): # test without error handler app = TestApp(make_app(RootController(), template_path=self.template_path)) + r = app.post('/', { + 'colors-0' : 'blue', + 'colors-1' : 'red' + }) + assert r.status_int == 200 + assert r.body == '' + + # test failure without error handler r = app.post('/', { 'colors-0' : 'blue', 'colors-1' : '' @@ -669,7 +680,7 @@ class TestValidation(object): assert r.status_int == 200 assert r.body == 'Please enter a value' - # test with error handler + # test failure with error handler r = app.post('/with_handler', { 'colors-0' : 'blue', 'colors-1' : '' @@ -677,7 +688,7 @@ class TestValidation(object): assert r.status_int == 200 assert r.body == 'Please enter a value' - # test JSON without error handler + # test JSON failure without error handler r = app.post('/json', dumps({ 'colors-0' : 'blue', 'colors-1' : '' @@ -685,7 +696,7 @@ class TestValidation(object): assert r.status_int == 200 assert r.body == 'Please enter a value' - # test JSON with error handler + # test JSON failure with error handler r = app.post('/json_with_handler', dumps({ 'colors-0' : 'blue', 'colors-1' : '' @@ -726,3 +737,48 @@ class TestValidation(object): }) assert r.status_int == 200 assert r.body == 'There was an error!' + + def test_validation_exception(self): + + if 'mako' not in builtin_renderers: + return + + class NameSchema(Schema): + name = validators.String(not_empty=True) + + class SubController(object): + @expose() + def _route(self, *args): + raise ValidationException('/success') + + class RootController(object): + + sub = SubController() + + @expose('mako:form_name.html') + def errors_name(self): + return dict() + + @expose(schema=NameSchema(), + error_handler='/errors_name', + htmlfill=dict(auto_insert_errors=True)) + def name(self, name): + raise ValidationException(errors={'name': 'Names must be unique'}) + + @expose() + def success(self): + return 'Success!' + + def _get_contents(filename): + return open(os.path.join(self.template_path, filename), 'r').read() + + # test exception with no controller + app = TestApp(make_app(RootController(), template_path=self.template_path)) + r = app.get('/sub') + assert r.status_int == 200 + assert r.body == 'Success!' + + # test exception with additional errors + r = app.post('/name', {'name': 'Yoann'}) + assert r.status_int == 200 + assert r.body == _get_contents('form_name_invalid_custom.html')