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'))
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,
ValidationErroris raised. This
ValidationErrorin turn is converted to
WidgetInputErrorand 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 categoriesIf 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
- Widget level errors, which are associated with each widget and displayed along with them.
- 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 formIn 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
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 formWidgets 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
As there is already an
InvalidErrorViewobject which implements
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
FormBaseclass 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.