views:

197

answers:

3

What is the proper way to handle errors with Python + Pylons?

Say a user sets a password via a form that, when passed to a model class via the controller, throws an error because it's too short. How should that error be handled so that an error message gets displayed on the web page rather than the entire script terminating to an error page?

Should there be any error handling in the controller itself?

I hope I am explaining myself clearly.

Thank you.

+1  A: 

What are you using to validate your forms? I'm using formalchemy. It validates the input data using built-in and custom validators, and feeds a list with the errors it finds. You can then display that list in any way you want in your template.

Documentation here.

nosklo
A: 

I use formencode @validate decorator. It is possible to write custom validator for it but the problem with handling exceptions that occur in the model after validation still exists.

You could write custom action decorator similar to formencode one which will handle your model exceptions and populate c.form_errors.

Yaroslav
Do you by chance have any sample code that you could include to illustrate how you do this? Thank you!
ensnare
I've added code sample in a separate answer
Yaroslav
+1  A: 

Not pretending to be production-ready solution, just as an example. Actually I've copied most of decorator code from pylons.decorators:

from decorator import decorator
from webob import UnicodeMultiDict

class ModelException(Exception):
    pass

def handle_exceptions(form):
    def wrapper(fn, self, *args,**kwargs):
        try:
            return fn(self, *args, **kwargs)
        except ModelException, e:
            errors = str(e)
            params = request.POST

            is_unicode_params = isinstance(params, UnicodeMultiDict)
            params = params.mixed()


            request.environ['pylons.routes_dict']['action'] = form
            response = self._dispatch_call()
            # XXX: Legacy WSGIResponse support
            legacy_response = False
            if hasattr(response, 'content'):
                form_content = ''.join(response.content)
                legacy_response = True
            else:
                form_content = response
                response = self._py_object.response

            # If the form_content is an exception response, return it
            if hasattr(form_content, '_exception'):
                return form_content

            form_content = htmlfill.render(form_content, defaults=params,
                                           errors=errors)
            if legacy_response:
                # Let the Controller merge the legacy response
                response.content = form_content
                return response
            else:
                return form_content
    return decorator(wrapper)


class HelloFormSchema(Schema):
    allow_extra_fields = True
    filter_extra_fields = True
    name = formencode.validators.UnicodeString(not_empty=True)
    email = formencode.validators.UnicodeString(not_empty=True)

class HelloController(BaseController):
    def new(self):
        return render('/hello/new.html')

    def view(self):
        return 'created'

    @handle_exceptions(form='new')
    @validate(schema=HelloFormSchema(), form='new')
    @restrict("POST")
    def create(self):
        #here is code interacting with model which potentially could raise exception:
        if self.form_result['name'] == 'doe':
            raise ModelException('user already exists!')
        return redirect(url(controller='hello', action='view'))

new.html:

${h.form(h.url(controller='hello', action='create'), 'post')}
<dl>
    <dt>Name</dt>
    <dd>${h.text('name')}</dd>
    <dt>Email</dt>
    <dd>${h.text('email')}</dd>
    <dd>
    ${h.submit('create', 'Create')}
    </dd>
</dl>
${h.end_form()}
Yaroslav