I’ve been developing complex forms for a Plone project these days, and I get the job done with z3c.form, the form framework that was first used in the Plone world by the Singing & Dancing project.
Here is the first episode of a small serie to show interesting tips & tricks and patterns that I’ve been learning in the process.
To get started, install your buildout using fake eggs and the required dependencies (plone.z3cform, z3c.form…) You might want to follow the howto contributed by Daniel Nouri on plone.org.
(Note: The code snippets are simplified for easier reading.)
First things first !
z3c.form’s first concept, as you guess, is the “form”, which basically has the list of fields defined for it using an attribute called ‘fields’. From the form, it is also possible to access the list of field widgets using the ‘widgets’ attribute.
The framework is smartly engineered using Zope Component Architecture, so you have “separation of concerns” in every bit, and works with zope.schema for the fields definition and validation.
Note that, at least with the core, you can define a basic Form or a specific Add/Edit/DisplayForm, and other cases such as Group (a group of fields part of a Form) and Subform.
To define the list of fields for a form class, we must provide a schema (for example, IContactData), which I like to think of as the “data model” specification.
# my.example/my/example/interfaces.py
from zope.interface import Interface, Invalid, invariant
from zope import schema
class IContactData(Interface):
"""Contact data interface
"""
firstname = schema.TextLine(title=u"Firstname",
required=True)
lastname =schema.TextLine(title=u"Lastname",
required=True)
email =schema.TextLine(title=u"Email",
required=False)
@invariant
def email_format(obj):
if obj.email.find('@') == -1:
raise Invalid(u"Not a valid email")
Defining a storage… if you need to
Since, we generally need to store something, let’s choose the data storage. Though you could choose to do that later.
There are many options (including RDB-based), but the immediate one for us is using the ZODB.
The most simple way to do that, in most real-world apps, is by defining an object class that brings persistency, traversal, security and all the goodies we get in Zope for “free”, i.e. by inheriting from OFS.SimpleItem.SimpleItem (or OFS.Folder.Folder if we want containment) and defining the attributes it needs.
# my.example/my/example/contact.py
from zope import interface
from zope.schema.fieldproperty import FieldProperty
import OFS
from my.example.interfaces import IContactData
class MyContact(OFS.SimpleItem.SimpleItem):
"""Contact model class, with ZODB-based storage.
"""
interface.implements(IContactData)
firstname = FieldProperty(IContactData['firstname'])
lastname = FieldProperty(IContactData['lastname'])
email = FieldProperty(IContactData['email'])
def __init__(self, id, **kw):
self.id = id
for key, value in kw.items():
setattr(self, key, value)
super(MyContact, self).__init__(id)
@property
def title(self):
return "%s %s" % (self.firstname, self.lastname)
What I like in this pattern: It’s simple, it’s pythonic ! And it does the job for most cases where we don’t need a full-featured Plone content type.
Of course, if we need to manage a full-featured content type, we can inherit from plone.app.content base classes and bring the required Plone mechanisms on the table.
For those who are new to zope.schema, the Field Property mechanism, is very handy too. It does the job of providing data validation based on the information found in the schema.
Defining an “add” form for our Contact objects
Now the really new stuff starts !
An AddForm (and EditForm) could be used by a Content Manager to maintain a list of contacts in a folder within the site. You do that by inheriting from z3c.form.form.AddForm and providing your create(), add() and nextURL() methods.
# my.example/my/example/browser.py
import datetime
from zope import schema
import zope.component
import z3c.form
from plone.z3cform import base
from plone.i18n.normalizer.interfaces import IIDNormalizer
from my.example import interfaces
from my.example.contact import MyContact
class ContactAddForm(z3c.form.form.AddForm):
""" Contact Add Form """
fields = z3c.form.field.Fields(interfaces.IContactData)
def create(self, data):
id = data['firstname'] + data['lastname']
id = zope.component.queryUtility(IIDNormalizer).normalize(id)
self._name = id
contact = MyContact(self._name, **data)
return contact
def add(self, obj):
# Add the object within context, and persist it !
context = self.context
context[self._name] = obj
def nextURL(self):
return "%s/%s" % (self.context.absolute_url(), self._name)
There is one last thing to do, to make sure that our form can be rendered via Plone’s presentation machinery like any other page ; we define a special View by inheriting from the FormWrapper class provided by the plone.z3cform package, as follows :
# my.example/my/example/browser.py
class ContactAddFormView(base.FormWrapper):
form = ContactAddForm
label= "Contact Add Form"
And don’t forget to add the configuration for this in the right ZCML file, something along the lines of:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:five="http://namespaces.zope.org/five"
i18n_domain="my.example" >
<browser:page
for="Products.CMFPlone.Portal.PloneSite"
name="contact_add_form"
class=".browser.ContactAddFormView"
permission="cmf.AddPortalContent"
/>
</configure>
Update: Of course, we render our form after restarting Zope, via the URL http://plonesite/@@contact_add_form.
Of course, in a real case you would use another context for the container, by affecting the right interface to the ‘for’ attribute in the configure.zcml, instead of the ‘Products.CMFPlone.Portal.PloneSite’ value.
