Using `z3c.form`_ to build custom forms
Until now, we have used Dexterity’s default content add and edit forms, supplying form hints in our schemata to influence how the forms are built. For most types, that is all that’s ever needed. In some cases, however, we want to build custom forms, or supply additional forms.
the plone.z3cform package requires that standard z3c.form forms are used via a form wrapper view. In Dexterity, this wrapper is normally applied automatically by the form grokkers in plone.directives.form and plone.directives.dexterity.
Dexterity also relies on plone.autoform, in particular its AutoExtensibleForm base class, which is responsible for processing form hints and setting up z3c.form widgets and groups (fieldsets). A custom form, therefore, is simply a view that uses these libraries, although Dexterity provides some helpful base classes that make it easier to construct forms based on the schema and behaviors of a Dexterity type.
If you want to build standalone forms not related to content objects, see the z3c.form documentation. For convenience, you may want to use the base classes and schema support in plone.directives.form.
An edit form is just a form that is registered for a particular type of content and knows how to register its fields. If the form is named edit, it will replace the default edit form, which is registered with that name for the more general IDexterityContent interface.
Dexterity provides a standard edit form base class that provides sensible defaults for buttons, labels and so on. This should be registered for a type schema (not a class). To create an edit form that is identical to the default, we could do:
class EditForm(dexterity.EditForm): grok.context(IFSPage)
The default name for the form is edit, but we could supply a different name using grok.name(). The default permission is cmf.ModifyPortalContent, but we could require a different permission with grok.require(). We could also register the form for a particular browser layer, using grok.layer().
This form is of course not terribly interesting, since it is identical to the default. However, we can now start changing fields and values. For example, we could:
Add forms are similar to edit forms in that they are built from a type’s schema and the schemata of its behaviors. However, for an add form to be able to construct a content object, it needs to know which portal_type to use.
You should realise that the FTIs in the portal_types tool can be modified through the web. It is even possible to create new types through the web that re-use existing classes and factories.
For this reason, add forms are looked up via a namespace traversal adapter alled ++add++. You may have noticed this in the URLs to add forms already. What actually happens is this:
Plone renders the add menu. - To do so, it looks, among other places, for actions in the
folder/add category. This category is provided by the portal_types tool.
A user clicks on an entry in the menu and is taken to a URL like /path/to/folder/++add++my.type.
- The ++add++ namespace adapter looks up the FTI with the given name, and gets its factory property.
- The factory property of an FTI gives the name of a particular zope.component.interfaces.IFactory utility, which is used later to construct an instance of the content object. Dexterity automatically registers a factory instance for each type, with a name that matches the type name, although it is possible to use an existing factory name in a new type. This allows administrators to create new “logical” types that are functionally identical to an existing type.
- The ++add++ namespace adapter looks up the actual form to render as a multi-adapter from (context, request, fti) to Interface with a name matching the factory property. Recall that a standard view is a multi-adapter from (context, request) to Interface with a name matching the URL segment for which the view is looked up. As such, add forms are not standard views, because they get the additional fti parameter when constructed.
- If this fails, there is no custom add form for this factory (as is normally the case). The fallback is an unnamed adapter from (context, request, fti). The default Dexterity add form is registered as such an adapter, specific to the IDexterityFTI interface.
The form is rendered like any other z3c.form form instance, and is subject to validation, which may cause it to be loaded several times.
Eventually, the form is successfully submitted. At this point:
- The standard AddForm base class will look up the factory from the FTI reference it holds and call it to create an instance.
- The default Dexterity factory looks at the klass [*] attribute of the FTI to determine the actual content class to use, creates an object and initialises it.
- The portal_type attribute of the newly created instance is set to the name of the FTI. Thus, if the FTI is a “logical type” created through the web, but using an existing factory, the new instance’s portal_type will be set to the “logical type”.
- The object is initialised with the values submitted in the form.
- An IObjectCreatedEvent is fired.
- The object is added to its container.
- The user is redirected to the view specified in the immediate_view property of the FTI.
|[*]||class is a reserved word in Python, so we use klass.|
This sequence is pretty long, but thankfully we rarely have to worry about it. In most cases, we can use the default add form, and when we can’t, creating a custom add form is no more difficult than creating a custom edit form. The add form grokker take care of registering the add view appropriately.
As with edit forms, Dexterity provides a sensible base class for add forms that knows how to deal with the Dexterity FTI and factory.
A custom form replicating the default would look like this:
class AddForm(dexterity.AddForm): grok.name('example.fspage')
The name here should match the factory name. By default, Dexterity types have a factory called the same as the FTI name. If no such factory exists (i.e. you have not registered a custom IFactory utility), a local factory utility will be created and managed by Dexterity when the FTI is installed.
Also note that we do not specify a context here. Add forms are always registered for any IFolderish context. We can specify a layer with grok.layer() and a permission other than the default cmf.AddPortalContent with grok.require().
If the permission used for the add form is different to the add_permission set in the FTI, the user needs to have both permissions to be able to see the form and add content. For this reason, most add forms will use the generic cmf.AddPortalContent permission. The add menu will not render links to types where the user does not have the add permission stated in the FTI, even if this is different to cmf.AddPortalContent.