Using z3c.form for our forms in Plone

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.

Advertisements

5 Responses to Using z3c.form for our forms in Plone

  1. […] A variation on my previous z3c.form example Another example of form logic, adding to the one discussed in my previous post. […]

  2. nouri says:

    Since plone.app.z3cform 0.3, you need to define your form wrapping view like this:

    # my.example/my/example/browser.py
    from plone.app.z3cform import layout
    ContactAddFormView = layout.wrap_form(form, label=”Contact Add Form”)

    This is instead of:

    # my.example/my/example/browser.py
    class ContactAddFormView(base.FormWrapper):
    form = ContactAddForm
    label= “Contact Add Form”

    Note the change from plone.z3cform to plone.app.z3cform. The how-to on plone.org has been updated with these changes.

    For more details about the changes, please refer to http://plone.org/support/forums/addons#nabble-td580600

  3. nouri says:

    This wasn’t supposed to be a smiley 😉 in that code but a closing parenthesis. Also, it’s ‘ContactAddForm’, not ‘form’ as the first argument:

    # my.example/my/example/browser.py
    from plone.app.z3cform import layout
    ContactAddFormView = layout.wrap_form(
    ContactAddForm, label=”Contact Add Form”)

  4. cjai says:

    Hello, do you have any example how to create content with plone.app.content? I use item class from plone.app.content instead OFS. My code is like this :

    from plone.app.content import item
    class MyContact(item.Item):

    That code is instead of :

    import OFS
    class MyContact(OFS.SimpleItem.SimpleItem):

    But I got error in this url : http://paste.plone.org/23747

    Do you have any clue how to solve it? Thank you.

  5. kamon says:

    cjai: Since your content would inherit from CMF/Plone base content classes, I think you have to provide your special “add” handle method, similar to the handle_send() in my second example (ContactForm), and you need to use the entry point that CMFPlone offers to create an object, “the CMFPlone way”::

    from Products.CMFPlone.utils import _createObjectByType

    _createObjectByType(‘MyContact’, self.context, id=id,
    title= data[‘firstname’] + ‘ ‘ + data[‘lastname’], )

    Note that this is in theory ; I have previously found a bug with _createObjectByType unable to create correctly Z3-based content type objects (which is the case of plone.app.content-based types), because of the way their factory work, and I used a patch to make it work (as an example, if you face the same problem, you can see here: http://tinyurl.com/47u8yf).

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: