Friday, 9 November 2012

Adding custom validation in zope.formlib


If you are using ZTK, you would be familiar with zope.formlib and zope.schema libraries. These libraries provide a powerful mechanism to generate CRUD forms. In this, basic validation options can be specified in the interface via schema fields and by the constraint and invariant functions. I am not going to dive into those details as the documentation for it already exists on the Internet.

Let us consider a trivial example using Grok, in which, we shall try to check the validity of a form field

import grok
from zope import interface, schema

class IUser(interface.Interface):
    id = schema.BytesLine(title=u'User ID', required=True)

class AddUser(grok.AddForm):
    grok.name('add_user')
    form_fields = grok.AutoFields(IUser)
    
    @grok.action('Add')
    def add(self, **data):
        user = User()
        self.applyData(user, **data)
        self.context.add(user)
        self.redirect(self.url(self.context, 'list'))

The context for AddUser and the add() method of it are left to the reader's imagination ;)

As per the schema definition, zope.formlib will not allow the User's 'id' field in the add form to be empty. But, unfortunately it will accept spaces as a valid entry. This can be validated by adding an invariant to check the field's striped value. But, for the sake of simplicity we will use this example to enforce our custom validation.

Under the hood


Form's input and display elements are represented by formlib's widgets. There are default set of widgets associated with each schema field. On form submission, formlib tries to get the values from these widgets during which error checking is done based on the schema field's definition and then invariant validation is done. For our case, it checks whether the 'id' field is empty or not. If it is empty, ValidationError is raised. This ValidationError in turn is converted to WidgetInputError and appended to an error-list. Thereby, this error-list contains the list of all validation errors found in the form. An empty list indicates that there were no errors.
Error categories
If the form contains errors, the input fields will not be cleared and the error messages will be displayed. The errors are divided into two categories
  1. Widget level errors, which are associated with each widget and displayed along with them.
  2. Top level errors, which are usually displayed on the top of the form just below the status message. It contains both the invariant error messages and widget error messages.
Displaying top-level errors in form
In order to display the top-level error messages, formlib takes each error in the error-list displays them, as it is, if it is a string. If the error is not of type string, it gets the MultiAdapter view for the browser request and error to the interface IWidgetInputErrorView.

view = component.getMultiAdapter((error, self.request), 
                                 IWidgetInputError)

The error is displayed by calling the snippet() method on this view, which in turn, returns the error message as HTML snippet. 
Displaying widget-level errors in form
Widgets on the other hand, have an error state, which can be rendered in a form using its error() method. Whenever the error() method is called, it checks its internal error state, if there is an error it gets the multi-adapter view as mentioned above and displays the error by calling the snippet() method.

Implementation


As there is already an InvalidErrorView object which implements IWidgetInputErrorView and adapts interface.Invalid and IBrowserRequest, we shall replace the Widget's error() method with a callable error string for our induced errors.

Enough of Zope's internals, lets override the validate method of zope.formlib's FormBase class and write a helper function to set the error.

def set_form_error(widgets, wgt_name, emsg, elist):
    wgt = widgets.get(wgt_name)
    if wgt is None:
        raise interface.Invalid("Invalid widget (%s)" % (wgt_name,))
    if isinstance(elist, list) is False:
        raise interface.Invalid("Error list invalid")

    # We override the Widget's 'error' method
    wgt.error = lambda : emsg
    # Here we append the error message as string in the list.
    # As this corresponds to top-level error messages we
    # mention the widget's label in it so that the error
    # message can be meaningful.
    elist.append("%s: %s" % (wgt.label, emsg))

class AddUser(grok.AddForm):
    grok.name('add_user')
    form_fields = grok.AutoFields(IUser)

    def validate(self, action, data):
        ret = super(AddUser, self).validate(action, data)
        # If the form has basic errors we just display them
        if len(ret) != 0: return ret
        # If the form has no errors we do additional validation
        data['id'] = data['id'].strip()
        if len(data['id']) == 0:
            # Here we induce our custom error
            set_form_error(self.widgets, 'id', 'Invalid', ret)
        return ret

    @grok.action('Add')
    def add(self, **data):
        user = User()
        self.applyData(user, **data)
        self.context.add(user)
        self.redirect(self.url(self.context, 'list'))
 
I guess, the above piece of code is fairly simple now.

No comments:

Post a Comment