Using zope.formlib

« Return to page index

Plone Developer Manual is a comprehensive guide to Plone programming.

1. Why learn how to use a new framework?

You may be wondering why should you learn how to use a new forms framework if you already know how to use the CMF Form Controller Tool (Form Controller).

Why should you use a forms framework at all? You could always write your own HTML form snippets and use the request dictionary to retrieve and handle data.

The reason is simple: you'll end up writing a lot of boilerplate code to collect, validate and build the response. It would be better if you could just define the fields and metadata of the form and re-use a set of base classes to do the repetitive work behind the scenes, i.e., a forms framework.

One of these frameworks is the Form Controller Tool, which is not bad, but has some disadvantages over formlib:

  • First, the Form Controller spreads the form logic across several files so it can be hard to follow it.
  • Second, the From Controller doesn't handle the creation and display of the widgets, so you have to create them manually, what could become especially unmantainable when using choice-type fields.
  • Last, the Form Controller doesn't work with Zope 3 schema interfaces nor views. Using a Zope 3 schema can help you creating add and edit forms.

However, the Form Controller can be useful and even preferable when you need to implement a complex page flow, or if you want to customize Plone forms that use it; e.g. the ''Send this page to someone'' form.

Beginning with Zope 2.9.3 (Plone 2.5) zope.formlib is being distributed with Zope 2. Five >= 1.4 is required to make use of this Zope 3 package.

Note: Where do I place the code?

You can place the code wherever you want: all in the same file, each class in a file, in several directories, utilities in a utilities.py file, etc. Just keep in mind two things:

  • If you write several pieces of code (functions, classes) in separate files, remember to import them whenever you use them, as you would do in any other Python program.
  • The ZCML statements have to be placed into a file called configure.zcml in the root of your package, or in any other file included from it.

Said that, the author reccommends putting all the Python code in a file named browser.py in this tutorial to avoid confusion.

2. Creating a simple feedback form

This section explains how to create a very basic feedback form.

The code for this example is available to checkout from the collective as the example.formlib package.

For all practical sense formlib based components are really regular Zope view components with some convenient base classes for auto-generating output based on schemas and other configuration info. You will see that in a moment.

First, define an interface class with the schema of the form:

from zope.interface import Interface
from zope.schema import TextLine, Text

class IFeedbackForm(Interface):
    """
    A typical feedback schema
    """
    customer = TextLine(title=u'Customer',
                      description=u'Customer email',
                      required=True)

    subject = TextLine(title=u'Subject',
                       required=True)

    message = Text(title=u'Message',
                   description=u'The message body',
                   required=True)

The purpose of this interface is to define the fields of the form. The type of each schema field determines the type of widget that will be used by default for that field, so choose it carefully. To see all the schema fields available, read the zope.schema package's interfaces.

Next, create a form instance, which is a class that groups an ordered collection of fields and actions. To do that, simply subclass Five's PageForm class, a wrapper to the formlib Form class to keep Zope 2 happy. Type the following code into a Python file inside your product:

from Products.Five.formlib.formbase import PageForm

You will also need to make use of Five's strange hybrid between Zope 2 and Zope 3 page templates:

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

The simplest way to define a collection of form fields is using the Fields constructor with the previous schema:

from zope.formlib import form

class FeedbackForm(PageForm):
    """
    A typical feedback form
    """
    form_fields = form.Fields(IFeedbackForm)

By inheriting from the PageForm class, the FeedbackForm class inherit functionality from formlib itself. By default, PageForm knows how to generate all the HTML that will make up of a finished form. But in order to do this, formlib needs to know what fields are wanted. This is done by providing the form_fields attribute. The Fields constructor is a formlib helper class that generates the appropriate field items from any Zope 3 schema (in this case, the schema interface defined above).

In order to provide a complete form, you need to specify the action to perform when the "submit" button of the form (or any other indicated) is activated. To define the action, use the form.action decorator with a handler function for the submitted data. More on actions later.

# use a dummy MailHost tool here to keep it simple
class MHost:
    def __init__(self):
        pass
    def Send(self, sender, to, subject, body):
        pass

class FeedbackForm(PageForm):
    """
    A typical feedback form
    """
    form_fields = form.Fields(IFeedbackForm)
    result_template = ViewPageTemplateFile('feedback_result.pt')

    @form.action("send")
    def action_send(self, action, data):
        mhost = MHost()
        self.mFrom = data['customer']
        self.mTo = "feedback@mycompany.com"
        self.mSubject = data['subject']
        self.mBody = data['message']
        mhost.Send(self.mFrom, self.mTo, self.mSubject, self.mBody)
        return self.result_template()

This is where the real work takes place. In this example, the feedback_result.pt page template is rendered and returned. All the view's attributes will be available inside this template, which will be introduced later.

An example result form is:

<html metal:use-macro="context/@@standard_macros/view">
     
<head>
</head>

<body>
    <div metal:fill-slot="body">
        <h1 tal:content="view/label">Form label</h1>
        <p>Thank you for your request about
        <span tal:replace="view/mSubject">subject</span>,
        <span tal:replace="view/mFrom">customer@mail</span>.</p>
        <p>We will reply to it shortly.</p>
    </div>
</body>
</html>

zope.formlib already includes a default general page form template, with the fields labels, the widgets structures and the submit buttons, so you only have to register your form page with the appropiate ZCML snippet in order to make it accesible from a browser. Assuming you've placed your code into a file named browser.py:

<browser:page
        name="feedback"
        for="Products.CMFPlone.Portal.PloneSite"
        class=".browser.FeedbackForm"
        permission="zope.Public"
        />
Let's explain what this ZCML snippet means:
  • The for attribute indicates the class or interface this view will be available for; in this case, it will be shown only from the root of a Plone site. To see the interfaces provided by a certain object, fire up the ZMI, navigate up to your object and check the Interfaces tab.
  • The name attribute sets the name of the view, so the form will be available from a URL with the form http://<plone-site>/feedback.
  • The class attribute indicates the view class responsible for displaying the page form, in this case, the FeedbackForm class inside the browser.py file.
  • The permission attribute specify the permission needed to access the page.

Among the most used permissions you can find:

  • zope.Public - no restrictions, available to everyone.
  • zope.View - permission to view this component.
  • zope.ManageContent - add, edit and delete content objects.

Note: Keen readers will notice the special name for configuring the new view component, browser:page. This XML tag actually employs an XML namespace prefix which needs to be defined. Normally this is added right onto the configure tag like this:

<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:five="http://namespaces.zope.org/five">

And that's all! Here's how the form and result pages will look like:

Contact form:

First form

Result page:

First result

 

3. Adding validation

Server-side form validation is vital to ensure data sanity and protect our site from malicious users.

Field validation

Once you've understood the "hello form", let's move onto a more advanced topic: validation.

The easiest way to manage validation in a formlib-based form is to specify the validation rules in our schema. Actually, you've already implemented some validation: the customer, subject and message fields are required. If you leave the subject field empty, for example, and click the send button, a pretty red error message will show up asking you to fill that field.

Let's add email validation to the customer field using the constraint keyword argument fot that attribute in our schema. For simplicity, the mail address checker that comes with the CMFDefault utilities toolbox will be used in this example, althought you could also use your own regular expression checking. The constraint argument must be a callable that returns True if the value submitted is valid, or raise an exception inheriting from zope.schema.ValidationError, whose docstring will be used in the error message.

from zope.schema import ValidationError

class InvalidEmailAddress(ValidationError):
    "Invalid email address"

from Products.CMFDefault.utils import checkEmailAddress
from Products.CMFDefault.exceptions import EmailAddressInvalid

def validateaddress(value):
    try:
        checkEmailAddress(value)
    except EmailAddressInvalid:
        raise InvalidEmailAddress(value)
    return True

class IFeedbackForm(Interface):
    """
    A typical feedback schema
    """
    customer = TextLine(title=u'Customer',
                      description=u'Customer email',
                      required=True,
                      constraint=validateaddress)

    subject = TextLine(title=u'Subject',
                       required=True)

    message = Text(title=u'Message',
                   description=u'The message body',
                   required=True)

Now, if you type an invalid address into the customer field and click send, a kind and colorful error message will be displayed:

Simple validation error

That was too easy, wasn't it? 

Invariants validation

zope.formlib also supports the validation of schema invariants, e.g. the min value entered must be smaller than the max value. In this example the form will be extended to provide a set of predefined subjects and a field named other which must be filled when selecting the the Other option in the subject select dropdown. It's easier to explain it in Python than in English:

from zope.schema import Choice
from zope.interface import invariant, Invalid

class IFeedbackForm(Interface):
    """
    A typical feedback schema
    """
    customer = TextLine(title=u'Customer',
                      description=u'Customer email',
                      required=True,
                      constraint=validateaddress)

    subject = Choice(title=u'Subject',
                   vocabulary='Available Subjects',
                   required=True,
                   )

    other = TextLine(title=u'Other',
                     description=u"""
                        If you've specified Other above,
                        please fill this this field too.""",
                        required=False)

    message = Text(title=u'Message',
                   description=u'The message body',
                   required=True)

    @invariant
    def otherFilledIfSelected(feedback):
        if feedback.subject == u'Other' and not feedback.other:
            raise Invalid("Please specify the motivation of your request")

Here, the subject field type has been set to Choice, and the list of available values has been indicated to be obtained from the Available Subjects vocabulary, a named utility which will be defined shortly.

The form will call all the invariant-decorated functions of the schema upon validation and catch any raised Invalid exceptions.

You still need to define the Available Subjects vocabulary:

from zope.schema.vocabulary import SimpleVocabulary

def availableSubjects(context):
    subjects = ('Comment',
                'Feature Request',
                'Technical Issue',
                'Complaint',
                'Other',
                )
    return SimpleVocabulary.fromValues(subjects)

and register it as a named utility using ZCML in the configure.zcml file:

<configure ... >
...
    <utility
            component=".browser.availableSubjects"
            name="Available Subjects"
            provides="zope.schema.interfaces.IVocabularyFactory"
            />
</configure>

Restart your Zope instance for the changes to take effect and test your new form. You'll see something similar to this:

Invariant error

Unfortunately, invariant errors descriptions are not shown in the default template.

4. Customizing the template and the widgets

Hack into the appearance of your form.

Customizing the template

plone.app.form provides a handy default template named pageform.pt which integrates well with the default Plone skin, but you might need to customize it or write your own one.

To do that, override the template attribute of the form class definition:

class FeedbackForm(PageForm):
    """
    A typical feedback form
    """
    label = u'Contact Us'
    form_fields = form.Fields(IFeedbackForm)
    template = ViewPageTemplateFile('feedback_form.pt')
    result_template = ViewPageTemplateFile('feedback_result.pt')

    @form.action("send")
    def action_send(self, action, data):
        mhost = MHost()
        self.mFrom = data['customer']
        self.mTo = "feedback@mycompany.com"
        self.mSubject = data['subject']
        self.mBody = data['message']
        mhost.Send(self.mFrom, self.mTo, self.mSubject, self.mBody)
        return self.result_template()

As already stated, all the view attributes will be available inside the page template, including:

  • label - A label to display at the top of the form.
  • prefix - A string added to all widget and action names.
  • form_fields - The list of form's fields.
  • widgets - A list of views for the former fields. The widgets are looked up as multiadapters for each schema field and the request providing IDisplayWidget or IInputWidget.
  • errors - A list of errors encountered during validation.
  • error_views - A list of views for the former errors. These views are looked up as multiadapters for each error and the request providing zope.app.form.browser.interfaces.IWidgetInputErrorView.
  • status - An update status message, normally generated by success or failure handlers.
  • availableActions - The list of form's available actions.
  • template - The template used to display the form.

It's reccommended to start with the default pageform.pt and customize it cutting, pasting, deleting and entering text and tags.

Using named templates

Another really zope3-ish method to choose the form template is using the zope.formlib named templates. Using named templates can be (and actually is) an overkill if you've designed your template to work with your form class as a single component. But if you write a form class and the template is just a visual customization of that form, you might want to be able to customize the template without having to reimplement the whole class, or let others do so. This is exactly how Plone overrides the default zope.formlib template with a more plone-ish one in the plone.app.form package.

Please note that this approach was not taken in the example product example.formlib.

Named templates are adapters for the form's view class to INamedTemplate, bound to the form class only by their names. This way, a third party product (e.g. a theme) can register a different template with the same name (usually in a different browser skin layer) to override the default one. Moreover, they're very easy to use. Modify and add the emphasized lines:

from zope.formlib.namedtemplate import NamedTemplate
# Five's ViewPageTemplateFile doesn't work correctly with formlib's NamedTemplateImplementation,
# so we use here the Plone implementation
from plone.app.form import named_template_adapter  

class FeedbackForm(PageForm):
    """
    A typical feedback form
    """
    label = u'Contact Us'
    form_fields = form.Fields(IFeedbackForm)
    template = NamedTemplate('feedback.form')
    result_template = ViewPageTemplateFile('feedback_result.pt')
    # rest of the form class implementation...

feedback_template = named_template_adapter(
    ViewPageTemplateFile('feedback_form.pt'))

In configure.zcml, add the following snippet to register the named template as an adapter for your form:

    <adapter
            factory=".browser.feedback_template"
            for=".browser.FeedbackForm"
            name="feedback.form"
            />

Name your page template feedback_form.pt and you're done.

Customizing the widgets

As we've already stated earlier, form widgets are views for schema fields, i.e. multiadapters for each schema field and the request providing IDisplayWidget or IInputWidget, depending on if they display field data or offer editing funcionality to the user.

To do so, override the custom_widget attribute of a field (which defaults to None). Remember how we set up the form's fields:

class FeedbackForm(PageForm):
    """
    A typical feedback form
    """
    label = u'Contact Us'
    form_fields = form.Fields(IFeedbackForm)
    # rest of the form class...

The form_fields fields are accessible throught a dict-like interface, with the schema field names as keys, so we write:

from zope.app.form.browser import RadioWidget as _RadioWidget

def RadioWidget(field, request):
    vocabulary = field.vocabulary
    widget = _RadioWidget(field, vocabulary, request)
    return widget 

class FeedbackForm(PageForm):
    """
    A typical feedback form
    """
    label = u'Contact Us'
    form_fields = form.Fields(IFeedbackForm)
    form_fields['subject'].custom_widget = RadioWidget
    # rest of the form class...

Here, we're specifying a custom widget for the subject field: RadioWidget, which displays a radio box for every item from the field's vocabulary. The zope.app.form.browser and plone.app.form.widgets packages provide a reasonable set of widgets to use and customize, including dropdowns and Kupu/WYSIWYG. Unfortunately, creating new widgets is out of the scope of this tutorial for now.

The RadioWidget function deserves a little explanation. Believe it or not, zope.formlib doesn't handle custom widgets with vocabularies (called items widgets) properly, because it calls form_field.custom_widget(field, request) either the field has an associated vocabulary or not, and item widgets have to be initialized with a vocabulary argument too; so a wrapper function is needed to workaround this issue.

Here's how the improved form looks like:

Form radio buttons