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:
- Create a profile directory structure that has
profiles/default/types beneath our formlib directory.
- Construct a new types.xml file underneath the default directory.
- Create a file with the name FormlibExampleContent.xml underneath
the types directory.
- 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.