Using z3c.form forms in Plone

« Return to page index

z3c.form is an advanced form and widget framework for Zope and Plone. It provides an easy and flexible way to display forms, to handle form creation, validation, and actions.

Introduction

What is z3cform and why should I consider using it.

What is plone.app.z3cform?

plone.app.z3cform is a package that provides widgets and other utilities for making forms in Plone. It also contains templates for its big brother, the plone.z3cform package. plone.z3cform is a very thin layer around z3c.form. Forms that you make with plone.app.z3cform are essentially the same as pure Zope 3 forms built with z3c.form. No strings attached, and no special Zope 2/Plone hacks required (like self.context.aq_inner). z3c.form comes with excellent documentation.

z3c.form vs. Archetypes

Archetypes forms are hard to use independently of content types. This has led to several hacks in the past, where Archetypes content types were misused as tools, survey forms etc.

z3c.form vs. formlib

zope.formlib is often too inflexible and hacky, due to its lack of adaptability and lack of a solid set of widgets.

What will I learn in this tutorial?

This tutorial will show how to create a simple z3c.form comment form which we will be extended and and displayed inside a viewlet.

Further information

You can find further information on the PyPi pages of plone.z3cform and plone.app.z3cform.

The new Plone Developers Manual also comes with z3c.form documentation.

The sourcecode of this example is available in the collective.

In addition, z3c.form has been used extensively used for the Singing & Dancing newsletter add-on and plone.app.discussion. In the sourcecode of these packages you can find tons of examples for z3c.forms.

Creating a buildout for z3c.form development

A few things you need before we can get started.

Before we can start creating a z3c.form inside Plone, we need to create a Plone buildout with the necessary package dependencies. Make sure you have Python, Setuptools, and ZopeSkel installed. Instructions can be found in the buildout tutorial. You should also make sure that you have the latest version of ZopeSkel installed. You can update your ZopeSkel with

$ easy_install-2.4 -U ZopeSkel

Create a buildout

At first we create a new Plone buildout with the paster script:

$ paster create -t plone3_buildout z3cformtutorial-buildout

Just hit return on all questions to choose the default values. Though, you can set your own zope password.

Selected and implied templates:
 ZopeSkel#plone3_buildout  A buildout for Plone 3 projects

Variables:
 egg:      z3cformtutorial_buildout
 package:  z3cformtutorialbuildout
 project:  z3cformtutorial-buildout
Enter plone_version (Which Plone version to install) ['3.3.1']: 
Enter zope2_install (Path to Zope 2 installation; leave blank to fetch one) ['']: 
Enter plone_products_install (Path to directory containing Plone products; leave blank to fetch one) ['']: 
Enter zope_user (Zope root admin user) ['admin']: 
Enter zope_password (Zope root admin password) ['']: admin
Enter http_port (HTTP port) [8080]: 
Enter debug_mode (Should debug mode be "on" or "off"?) ['off']: 
Enter verbose_security (Should verbose security be "on" or "off"?) ['off']: 
Creating template plone3_buildout
Creating directory ./z3cformtutorial-buildout
 Copying README.txt to ./z3cformtutorial-buildout/README.txt
 Copying bootstrap.py to ./z3cformtutorial-buildout/bootstrap.py
 Copying buildout.cfg_tmpl to ./z3cformtutorial-buildout/buildout.cfg
 Recursing into products
 Creating ./z3cformtutorial-buildout/products/
 Copying README.txt to ./z3cformtutorial-buildout/products/README.txt
 Recursing into src
 Creating ./z3cformtutorial-buildout/src/
 Copying README.txt to ./z3cformtutorial-buildout/src/README.txt
 Recursing into var
 Creating ./z3cformtutorial-buildout/var/
 Copying README.txt to ./z3cformtutorial-buildout/var/README.txt
-----------------------------------------------------------
Generation finished
You probably want to run python bootstrap.py and then edit
buildout.cfg before running bin/buildout -v

See README.txt for details
-----------------------------------------------------------

Pin the required package versions for z3c.form

In order to make z3c.form work in Plone, we need to install certain packages with a specific set of package versions. To make things easy, we extend our buildout with the Known Good Set of plone.autoform to do so. Just add "http://good-py.appspot.com/release/plone.autoform/1.0b2" to the extends line of your buildout.

buildout.cfg

extends = 
    http://good-py.appspot.com/release/plone.autoform/1.0b2
...

For Plone 4, we don't need a KGS. It is sufficient if we just pin zope.schema in the buildout.cfg:

[versions]
zope.schema = 3.6.0

Run buildout

After pinning down the versions, we can run the buildout script

$ cd z3cformtutorial-buildout
$ python bootstrap
$ ./bin/buildout

Now we are ready to start creating a Python package that contains the form we are going to create in the next step.

Creating a z3c.form Python Package

We now create a Python package containing a simple form.

In order to create a new Python package, go to the src directory of your buildout and let paster create the package for you. Add "example" for namespace and "z3cformtutorial" for package.

$ cd src
$ paster create -t plone example.z3cformtutorial

Output

Selected and implied templates:
  ZopeSkel#basic_namespace  A project with a namespace package
  ZopeSkel#plone            A Plone project

Variables:
  egg:      example.z3cformtutorial
  package:  examplez3cformtutorial
  project:  example.z3cformtutorial
Enter namespace_package (Namespace package (like plone)) ['plone']: example
Enter package (The package contained namespace package (like example)) ['example']: z3cformtutorial
Enter zope2product (Are you creating a Zope 2 Product?) [False]: 
Enter version (Version) ['1.0']: 
Enter description (One-line description of the package) ['']: 
Enter long_description (Multi-line description (in reST)) ['']: 
Enter author (Author name) ['Plone Foundation']: 
Enter author_email (Author email) ['plone-developers@lists.sourceforge.net']: 
Enter keywords (Space-separated keywords/tags) ['']: 
Enter url (URL of homepage) ['http://svn.plone.org/svn/plone/plone.example']: 
Enter license_name (License name) ['GPL']: 
Enter zip_safe (True/False: if the package can be distributed as a .zip file) [False]: 
Creating template basic_namespace
Creating directory ./example.z3cformtutorial
  Recursing into +namespace_package+
    Creating ./example.z3cformtutorial/example/
    Recursing into +package+
      Creating ./example.z3cformtutorial/example/z3cformtutorial/
      Copying __init__.py_tmpl to ./example.z3cformtutorial/example/z3cformtutorial/__init__.py
    Copying __init__.py_tmpl to ./example.z3cformtutorial/example/__init__.py
  Copying README.txt_tmpl to ./example.z3cformtutorial/README.txt
  Recursing into docs
    Creating ./example.z3cformtutorial/docs/
    Copying HISTORY.txt_tmpl to ./example.z3cformtutorial/docs/HISTORY.txt
  Copying setup.cfg to ./example.z3cformtutorial/setup.cfg
  Copying setup.py_tmpl to ./example.z3cformtutorial/setup.py
Creating template plone
  Recursing into +namespace_package+
    Recursing into +package+
      ./example.z3cformtutorial/example/z3cformtutorial/__init__.py already exists (same content)
      Copying configure.zcml_tmpl to ./example.z3cformtutorial/example/z3cformtutorial/configure.zcml
      Copying tests.py_tmpl to ./example.z3cformtutorial/example/z3cformtutorial/tests.py
  Recursing into docs
    Copying INSTALL.txt_tmpl to ./example.z3cformtutorial/docs/INSTALL.txt
    Copying LICENSE.GPL to ./example.z3cformtutorial/docs/LICENSE.GPL
    Copying LICENSE.txt_tmpl to ./example.z3cformtutorial/docs/LICENSE.txt
Replace 1022 bytes with 1272 bytes (0/32 lines changed; 8 lines added)
  Copying setup.py_tmpl to ./example.z3cformtutorial/setup.py
------------------------------------------------------------------------------
The project you just created has local commands. These can be used from within
the product.

usage: paster COMMAND

Commands:
  addcontent  Adds plone content types to your project

For more information: paster help COMMAND
------------------------------------------------------------------------------
Running /usr/bin/python2.4 setup.py egg_info

Adding z3c.form dependencies to the package

Now we add plone.app.z3cform as dependency to your newly created Python package, plone.z3cform will be automatically fetched by plone.app.z3cform:

src/example.z3cformtutorial/setup.py

...    
    install_requires=[
        'setuptools',
        # -*- Extra requirements: -*-
        'plone.app.z3cform',
    ],
...

After this, we add our package to our buildout configuration

buildout.cfg

[buildout]
...
eggs =
    example.z3cformtutorial

...
develop =
    src/example.z3cformtutorial
    ...

[instance]

...

zcml =
    example.z3cformtutorial

Then, re-run buildout to fetch the dependencies:

$ ./bin/buildout

Now we are ready to actually create our first form.

Create a simple z3cform

Create a simple comment form.

First, we define a schema with a title, author, and text field for our comment form:

from zope import interface, schema

class IComment(interface.Interface):
title = schema.TextLine(title=u"Title")
author = schema.TextLine(title=u"Author",
required=False)
text = schema.TextLine(title=u"Text")

The comment form uses the schema definition to define and later render the form. Also we define a label that will show up above the form:

from z3c.form import form, field

class CommentForm(form.Form):
fields = field.Fields(IComment)
ignoreContext = True # don't use context to get widget data
label = "Add a comment"

Next, we add a submit button to the form with a method to handle the form data after we submitted the form. We extract the data from the form request, and return to the form if there are validation errors, otherwise we proceed:

from z3c.form import button
@button.buttonAndHandler(u'Post comment')
def handleApply(self, action):
data, errors = self.extractData()
if errors:
return
if data.has_key('text'):
print data['text'] # ... or do stuff

As last step, we have to wrap the form into a standard Plone page:

from plone.z3cform.layout import wrap_form
wrap_form(CommentForm)

If we put all these steps together, our comment.py file should look like this:

from zope import interface, schema
from z3c.form import form, field, button
from plone.z3cform.layout import wrap_form

class IComment(interface.Interface):
title = schema.TextLine(title=u"Title")
author = schema.TextLine(title=u"Author", required=False)
text = schema.TextLine(title=u"Text")

class CommentForm(form.Form):
fields = field.Fields(IComment)
ignoreContext = True # don't use context to get widget data
label = u"Add a comment"

@button.buttonAndHandler(u'Post comment')
def handleApply(self, action):
data, errors = self.extractData()
if data.has_key('title') and data.has_key('text'):
print data['title'] # ... or do stuff

CommentView = wrap_form(CommentForm)

For more z3c.form schema details see http://docs.zope.org/z3c.form/browser/README.html.

The only thing left to do in order to use our form is to register it in the configure.zcml:

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

<!-- Include z3c.form as dependency -->
<include package="plone.app.z3cform" />

<!-- Register the comment form -->
<browser:page
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
name="comment_form"
class=".comment.CommentView"
permission="zope2.View"
/>

</configure>

Start the Zope instance

$ ./bin/instance fg

Go to the ZMI and create a Plone instance with the name 'test' and the Generic Setup profile "Plone z3c.form support". Then, go to the following URL

http://localhost:8080/test/comment_form

Showing a z3c.form inside a Viewlet

Now we want to display the comment form in a Viewlet.

In order to show our comment form inside a Viewlet, we first create a new file called commentviewlet.py containing a standard Viewlet with an associated page template and a label:

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from plone.app.layout.viewlets import ViewletBase

class CommentViewlet(ViewletBase):
    index = ViewPageTemplateFile('commentviewlet.pt')
    label = 'Add Comment'

To actually show the comment form inside the Viewlet, we have to patch the request and the update method like this:

    def update(self):
        super(CommentViewlet, self).update()
        z2.switch_on(self, request_layer=IFormLayer)
        self.form = CommentForm(aq_inner(self.context), self.request)
        self.form.update()

Together with the necessary imports, our commentviewlet.py file should look like this:

from Acquisition import aq_inner

from z3c.form.interfaces import IFormLayer

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

from plone.app.layout.viewlets import ViewletBase

from plone.z3cform import z2

from example.z3cformtutorial.comment import CommentForm

class CommentViewlet(ViewletBase):
    index = ViewPageTemplateFile('commentviewlet.pt')
    label = 'Add Comment'
    def update(self):
        super(CommentViewlet, self).update()
        z2.switch_on(self, request_layer=IFormLayer)
        self.form = CommentForm(aq_inner(self.context), self.request)
        self.form.update()

Next, we create a new page template called commentviewlet.pt to display our form by calling the render method of the form:

<h2 tal:content="view/label">View Title</h2>
<div id="layout-contents">
  <div tal:replace="structure view/form/render" />
</div>

The only thing left to do is to register the new Viewlet in the configure.zcml:

<browser:viewlet
    name="comment_viewlet"
    for="Products.CMFCore.interfaces.IContentish"
    manager="plone.app.layout.viewlets.interfaces.IBelowContent"
    class=".commentviewlet.CommentViewlet"
    permission="zope2.View"
    />

Restart your Zope instance

$ ./bin/instance restart

and go to the following URL to see the new comment form Viewlet in action:

http://localhost:8080/test

plone.z3cform >= 0.6.0

If you run plone >= 0.6.0, the comment form has to provide the IWrappedForm interface. Otherwise Plone will raise a maximum recursion error. Add the bold code to make the comment form work for all plone.z3cform versions:

from Acquisition import aq_inner

from zope.interface import alsoProvides

from z3c.form.interfaces import IFormLayer

from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

from plone.app.layout.viewlets import ViewletBase

from plone.z3cform import z2

from example.z3cformtutorial.comment import CommentForm

# starting from 0.6.0 version plone.z3cform has IWrappedForm interface 
try:
    from plone.z3cform.interfaces import IWrappedForm 
    HAS_WRAPPED_FORM = True 
except ImportError: 
    HAS_WRAPPED_FORM = False


class CommentViewlet(ViewletBase):
    index = ViewPageTemplateFile('commentviewlet.pt')
    label = 'Add Comment'

    def update(self):
        super(CommentViewlet, self).update()
        z2.switch_on(self, request_layer=IFormLayer)
        self.form = CommentForm(aq_inner(self.context), self.request)
        if HAS_WRAPPED_FORM: 
            alsoProvides(self.form, IWrappedForm)        
        self.form.update()