Using Zope schemas with a complex vocabulary and multi-select fields
Using multi-select fields beyond the obvious simple examples is not well documented. This is my attempt to explain a way to do this.
For background I would like to explain a little about the setup of the project we needed this for. This product uses a sql database to store information. This information is then made available to zope by SQLAlchemy and Alchemist. This example depends heavily on the above products, but the basic idea as well as the example ObjectVocabulary should be useful to others.
We use SQLAlchemy's relational abilities to handle relations with other tables, for example, if table A has a field b_id that points to table B, SQLAlchemy creates an object A (corresponding with table A) with an attribute b that is a list of the referenced "objects" in table B.
We had one case where this relation was a many-to-many relation. We wanted to implement this as a selection of zero or more objects from a vocabulary of possible choices. This is where the multi-select comes in.
For simplicity I will not show the sqlalchemy definitions here and focus only on what we had to do to make zope work with the alchemist schemas. The SQLAlchemy documentation is very complete and explains this in detail.
Because SQLAlchemy is an object relational mapper and it deals in objects only (if you use it properly), we needed a vocabulary that holds objects, rather than the usual key-value pairs as implemented by SimpleVocabulary. This is to avoid unnecessary glue to map a simple key back to an object. The first thing we did was implement our own Vocabulary class:
from zope.interface import implements
from zope.schema.interfaces import IVocabulary, IVocabularyTokenized
from zope.schema.vocabulary import SimpleTerm
class ObjectVocabulary(object):
"""
Vocabulary implementation for alchemy content types.
Class is constructed as follows:
vocab = Vocabulary(ItemClass, "id", "description")
"""
implements(IVocabulary, IVocabularyTokenized)
def __init__(self, objects, primaryField, displayField):
self.primaryField = primaryField
self.displayField = displayField
self.objects = objects
def __iter__(self):
return iter([SimpleTerm(value=ob,
token=getattr(ob, self.primaryField),
title=getattr(ob, self.displayField)) for ob in self.objects])
def __len__(self):
return len(self.objects)
def __contains__(self, value):
return value in self.objects
def getQuery(self):
return None
def getTerm(self, ob):
if ob not in self.objects:
raise LookupError, value
return SimpleTerm(value=ob,
token=getattr(ob, self.primaryField),
title=getattr(ob, self.displayField))
def getTermByToken(self, token):
for ob in self.objects:
if str(getattr(ob, self.primaryField)) == str(token):
return SimpleTerm(value=ob,
token=getattr(ob, self.primaryField),
title=getattr(ob, self.displayField))
raise LookupError, token
This vocabulary encapsulates a list of objects and creates SimpleTerms for the original object, token and title values. The tokens and titles are of course used to render a form. When the user selects something, the token is used once again to get the original Term, and the value attribute of the Term gives you the object.
Next we needed a multi-select widget. Zope already includes MultiCheckBoxWidget, but this cannot be used as is, because the form machinery expects to instantiate widgets with two parameters (field and request) while MultiCheckBoxWidget also expects a vocabulary.
from zope.app.form.browser import MultiCheckBoxWidget as MultiCheckBoxWidget_
class MultiCheckBoxWidget(MultiCheckBoxWidget_):
def __init__(self, field, request):
MultiCheckBoxWidget_.__init__(self, field, field.value_type.vocabulary, request)
The exact reasons for this implementation should become clear in a moment.
In our interfaces.py, where we define the annotation used by alchemist, we define the properties of our multi-select field as follows:
from zope.schema.vocabulary import SimpleVocabulary
from zope.schema import Choice, Set
from ore.alchemist.annotation import TableAnnotation
from schema import TableATable
Annotation = TableAnnotation(
"TableA",
properties = {
'single': zschema.Choice(
title=u'Single',
vocabulary=SimpleVocabulary.fromItems([])),
'multi': Set(title=u'Multi',
value_type=Choice(
vocabulary=SimpleVocabulary.fromItems([])))
})
ITableATable = transmute(TableATable,
Annotation,
__module__="Products.example.content.tablea.interfaces" )
While I am not certain how you would do this in pure zope, the trick lies in the Set and Choice field-types. We're saying that 'single' is one of the items (objects in our example) provided by a vocabulary, and 'multi' is a set of choices from another vocabulary. For the moment we use blank vocabularies. You are of course welcome to insert your vocabulary at this point, but we chose not to do this as this sometimes gets us into trouble with circular imports.
Finally, right before we render our form, we modify the vocabulary and tell it what widget to use for multi:
fi = self.form_fields.get('single')
fi.field.vocabulary = ObjectVocabulary(objectListA, "id", "title")
fi = self.form_fields.get('multi')
fi.field.value_type.vocabulary = ObjectVocabulary(objectListB, "id", "title")
self.form_fields['multi'].custom_widget=MultiCheckBoxWidget
From examples on the internet it seems this is also possible in zcml:
<addform
name="add_foo"
label="Add Foo"
schema=".interfaces.IFoo"
fields="filetypes"
content_factory=".foo.Foo"
template="foo.pt"
permission="zope.Public"
>
<widget
field="filetypes"
class=".mywidgets.MultiCheckBoxWidget"
/>
</addform>
I certainly hope this helps someone to write an even better howto on the subject.






