Introduction
In Plone 4 & 5, a package called z3c.form
is used for forms. While it has quite extensive documentation, beginner-level tutorials for accomplishing specific (presumably simple) small tasks are hard to come by.
One such task might be using annotations for storing form data. Here, I describe how to accomplish just that with minimum effort while sticking to ‘the Zope/Plone way’.
Custom data manager registration
First, you have to register the built-in z3c.form.datamanager.DictionaryField
for use with PersistentMapping (for more on what z3c.form
data managers are and how they work, see for example my earlier post). This can be done by adding the following (multi-) adapter ZCML
registration to the configure.zcml
file of your Plone add-on package.
<adapter
for="persistent.mapping,PersistentMapping zope.schema.interfaces.IField"
provides="z3c.form.interfaces.IDataManager"
factory="z3c.form.datamanager.DictionaryField"
/>
This basically tells Plone to use the DictionaryField adapter for loading & storing form data whenever a form is returning a PersistentMapping
.
Using the built-in persistent.mapping.PersistentMapping
directly instead of subclassing it has the advantage that were you ever to refactor yout (sub)class, such as move or rename it, existing annotations using that (sub)class would break. Sticking to PersistentMapping (or some other built-in persistent.*
data structure is a way to avoid such problems.
Form customization
For the DictionaryField
data manager to do its job, the form has to provide it with the object that the form will save submitted form to and load editable data from: In our case, that would be a PersistentMapping
instance. For that purpose, the z3c.form forms have a getContent
method that we customize:
def getContent(self):
annotations = IAnnotations(self.context)
return annotations["myform_mapping_storage_key"]
This assumes the context object, be it Page, Folder or your custom type, is attribute annotatable and that its annotations have been initialized with a PersistentMapping keyed at “myform_mapping_storage_key”. Doing that is left as an exercise to the reader (see annotations). For a more standalone form, add a try/except block to the getContent method that checks for existence of “myform_mapping_storage_key” and upon failure, initializes it according to your needs.
So, with just those small pieces of custom code (adapter ZCML
registration & a custom form getContent
method), your form can use annotations for storage.
Use with custom z3c.form validators
If you don’t need to register custom validators for your form, you’re all set. But if you do, there are some further details to be aware of.
To start with, there are some conveniences to simplify use z3c.form validators, but for anything but the simplest validators, we’d want to create a custom validator class based on z3c.form.validators.SimpleValidator
. A validator typically just has to implement a custom validate()
method that raises zope.interface.Invalid
on failure; we’re not going into more details here.
Such a validator needs to be first registered as an adapter and then configured for use by setting so-called validator discriminators, via calling the validator.WidgetValidatorDiscriminators()
function.
Among others, it expects a context parameter (discriminator) that tells the validator machinery which form storage to use the validator for.
Given we were using plain PersistentMapping, this poses a problem: if there are other forms using the same storage class (and same field), the validator will be applied to them as well. What if we want to avoid that; perhaps the other form needs another validator, or no validator?
We could subclass PersistentMapping, but earlier we saw it’s not necessarily a good idea. Instead, we can declare that our particular PersistentMapping instance provides an interface, and then pass that interface as a context (discriminator) to WidgetValidatorDiscriminators()
Defining such an interface is as easy as:
from zope.interface import Interface, directlyProvides
class IMappingAnnotationFormStorage(Interface):
"automatically applied 'marker' interface"
Then update the getContent method introduced earlier to tag the returned PersistentMapping with the interface thus:
def getContent(self):
annotations = IAnnotations(self.context)
formstorage = annotations["myform_mapping_storage_key"]
directlyProvides(formstorage, IMappingAnnotationFormStorage)
With that in place, you can register different validators for forms that all use nothing but plain PersistentMapping for storage.
Finally, here’s a custom mixin for your forms that has some built-in conveniences for annotation storage use:
class AnnotationStorageMixin(object):
"mixin for z3c.forms for using annotations as form data storage"
ANNOTATION_STORAGE_KEY = "form_annotation_storage"
ASSIGN_INTERFACES = None
def getContent(self):
annotations = IAnnotations(self.context)
mapping = annotations[self.ANNOTATION_STORAGE_KEY]
if self.ASSIGN_INTERFACES:
for iface in self.ASSIGN_INTERFACES:
directlyProvides(mapping, iface)
return mapping
Note the two class variables ANNOTATION_STORAGE_KEY
& ASSIGN_INTERFACES
. Override them in your own form class to configure the mixin functionality for your specific case. If you use this mixin class for multiple different forms, you very likely want to at least have a unique storage key for each form.