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, 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- 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 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 interfaceIWidgetInputErrorView
.
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 itserror()
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