ServerZen.Net

Technology news centering around Python

Archive for July 2006

Using zope.formlib With Plone: Part 3

The second part in this series focused on demonstrating how a search form could be implemented with minimal fuss. This third part will show how to setup a content type using a Zope 3 schema which will be used to setup formlib based view and edit forms.

Traditional Archetypes Schema’s

By now the entire Plone and Plone add-on developer communities generally use the Archetypes framework to construct their content types. The Archetypes framework introduced its own concepts of schema’s, widgets, and fields which suited most developers fine. The problem is that shortly thereafter (perhaps even simultaneously) the Zope 3 community was already building their own constructs of these same concepts. The result was zope.interface, zope.schema, and zope.app.form. Eventually came zope.formlib that would help glue more of this together in a web UI environment.

Zope 3 Schema’s

A Zope 3 schema functions very closely to an Archetypes schema from a conceptual perspective. They both share the basic principles of defining the fields of a content type. One glaring difference is that is that a Zope 3 schema field has no direct association (within the schema) with any sort of UI logic. That is, it does not know what widget will be used to display itself on a web form. This help keeps content and UI concerns separate. Content, after all, should never need to know or care what it will actually look like in a user interface. It only cares about, well, data.

Our Example

For purposes of keeping things clean, we will break up some of our previous example code into more common python modules. This means moving our ISearch interface from the browser module into a new interfaces module.

A Content Schema

In Zope 3, schema’s are defined by constructing Zope 3 interfaces, much like the interface we created in the second part. What wasn’t made clear in the second part was that we actually created a schema to represent the search fields.

Lets begin by creating a new interface in the interfaces module called IExampleContent. This interface will have various fields and should look like this:

class IExampleContent(interface.Interface):
    title = schema.TextLine(title=u'Title',
                            required=True)

    description = schema.Text(title=u'Description',
                              required=False)

    funny = schema.Bool(title=u'Am I Funny?',
                        default=False,
                        required=True)

    really_funny = schema.Bool(title=u'Am I Really and Truly Funny?',
                               default=False,
                               required=True)

You can look at the resulting interfaces module to see what this file should end up looking like.

As mentioned earlier this example demonstrates that no UI properties are made available on the schema itself. All that is described about the schema and its fields is what is required to define the type of data that it represents.

Formlib Views

Now that we have the schema that describes what fields our content type will eventually have, we can use the same basic approach that the second part used to build views for this content.

The schema we just defined sets up a couple fields as being of type Bool. The default widget for a Bool field displays true/false as the selectable options. For purposes of our example, we need these to show yes/no instead so we’ll setup our own tweaked custom widget for this.

def YesNoWidget(field, request, true=_('yes'), false=_('no')):
    vocabulary = schemavocab.SimpleVocabulary.fromItems(((true, True),
                                                         (false, False)))
    return form_browser.RadioWidget(field, vocabulary, request)

What we have created here is a type of factory that will give us the widget we need based on an existing widget, RadioWidget. RadioWidget takes a vocabulary which we setup as having yes and no as items. Next we will define our form fields based on our content schema.

example_content_fields = form.FormFields(interfaces.IExampleContent)
example_content_fields['really_funny'].custom_widget = YesNoWidget

As with our example in the second part we generate the form fields using form.FormFields(). This class takes an interface as an argument and generates the form fields (the UI complement of a schema’s fields) we will use for our views. The second line here sets up our really_funny schema field to use our own custom widget.

All that’s left now is to actually define the default view and edit form for our content type.

class ExampleContentView(formbase.DisplayForm):
    form_fields = example_content_fields

    def __init__(self, *args, **kwargs):
        formbase.DisplayForm.__init__(self, *args, **kwargs)

        # a hack to make the content tab work
        self.template.getId = lambda: 'index.html'

class ExampleContentEditForm(formbase.EditForm):
    form_fields = example_content_fields

    def __init__(self, *args, **kwargs):
        formbase.EditForm.__init__(self, *args, **kwargs)

        # a hack to make the content tab work
        self.template.getId = lambda: 'edit.html'

Pretty basic. We defined ExampleContentView to extend formbase.DisplayForm which means formlib will understand that this is a regular display view so all widgets should be displayed in view mode. The edit form, ExampleContentEditForm was defined to extend formbase.EditForm so that formlib would know to display the widgets in edit mode. But this isn’t the only thing that formlib knows to do with the edit form. By default, formbase.EditForm defines a single apply action for its form. When this form is submitted with the apply action, formlib knows that it must actually update the current object (ie context) with the submitted values. And for those adventurous types, it should also be observed that a successfully submitted apply action will fire an IObjectModifiedEvent upon saving the submitted data.

Of course once these views have been defined they need to be registered with the Zope 3 component architecture. This is done with configure.zcml.

<browser:page
    name="index.html"
    for=".interfaces.IExampleContent"
    class=".browser.ExampleContentView"
    permission="zope2.View"
    />

<browser:page
    name="edit.html"
    for=".interfaces.IExampleContent"
    class=".browser.ExampleContentEditForm"
    permission="cmf.ModifyPortalContent"
    />

This zcml snippet shows that we have given our default view the name, index.html and our edit form the name, edit.html.

You can look at the browser module and configure.zcml to see the end result.

Content Type

So now that the schema and views have all been defined for our content type, its time to build the actual content type class. As a matter of best practise, we define this class in the content module.

class FormlibExampleContent(atapi.BaseContent):
    interface.implements(interfaces.IExampleContent)

    title = fieldproperty.FieldProperty(interfaces.IExampleContent['title'])
    description = fieldproperty.FieldProperty(interfaces.IExampleContent['description'])
    funny = fieldproperty.FieldProperty(interfaces.IExampleContent['funny'])
    really_funny = fieldproperty.FieldProperty(interfaces.IExampleContent['really_funny'])

The first line after the class statement ensures that our new content class implements the IExampleContent interface which we defined in the interfaces module earlier (a schema is just an interface with zope.schema fields). The remaining lines setup python properties for each of the required fields. As an example, the funny line says to define a funny attribute that is modelled after the funny schema field in IExampleContent). This ensures some basic validation is setup on the content type itself. If someone where to have an instance of this content type with name, myobj and tried to do myobj.funny = ‘foo’ then a validation error would be raised because the value ‘foo’ is not of type Bool. Bool types expect true or false.

That’s it for the content type class itself. Take a look at the finished content module to see the end result.

Content Type Cataloguing

One neglected aspect of all of this is since we’re no longer using Archetypes auto-generated forms our content is no longer getting catalogued. In the world of Zope 3 such things would be accomplished by using events. Since formlib will fire off an IObjectModifiedEvent when a successful save has taken place, all that is left to us is to define a handler for that event.

def catalog_content(obj, event):
    obj.reindexObject()

The handler itself is quite simple, it takes as arguments the actual object and the event (in this case the IObjectModifiedEvent instance). Since we have the object in hand at this point, we merely call reindexObject().

Of course we still have to hook this up with the Zope 3 component architecture which takes us back to configure.zcml.

<subscriber
    for=".interfaces.IExampleContent
         zope.app.event.interfaces.IObjectModifiedEvent"
    handler=".content.catalog_content"
    />

This basically says we want to handle any IObjectModifiedEvent that has been fired with an instance of IExampleContent as the target object. In our case, FormlibExampleContent implements the IExampleContent interface so we know our handler will get called when it has been modified.

The only thing that remains now is to register our content type with CMF/GenericSetup.

Content Type Installation With GenericSetup

Since we’re using Plone 2.5 with these examples we will use the new GenericSetup tool to setup our new content type in a Plone site. More information specifically about GenericSetup and Plone can be read in Rob Miller’s excellent Understanding and Using GenericSetup in Plone tutorial.

Basic steps for setting up our content type with GenericSetup are:

  1. Create a profile directory structure that has profiles/default/types beneath our formlib directory.
  2. Construct a new types.xml file underneath the default directory.
  3. Create a file with the name FormlibExampleContent.xml underneath the types directory.
  4. Register a new extension profile that uses these files with GenericSetup in our main __init__.py file.

Actual construction of these files goes beyond the scope of this writing, but the contents of these files can be found starting at the base formlib directory.

Just remember to activate these content types in Plone 2.5 you would go to the portal_setup tool, select ploneexample.formlib sample content as the active site configuration, and then run all import steps.

Conclusion

Zope 3 schema’s in Plone has arrived. And thanks to zope.formlib we have auto-generated views and forms to boot. The widget and field sets to choose from in Zope 3 are a lot smaller than in the Archetypes world, but Zope 3 is quickly catching up.

The only main missing piece (from formlib’s perspective) here is using an auto-generated add form. While we can build those, they can’t easily be hooked into Plone as Plone feels it needs to create the content first before displaying any forms.

Jul 9, 2006 9:14:00 PM by rocky, 0 comments