Object lists with Grok and formlib
16 June 2008 15:10Today I needed an attribute on an object to be a list of contacts. Something like this:
from zope.interface import Interface
from zope import schema
from persistent import Persistent
from persistent.list import PersistentList
import grok
class IFacilityContact(Interface):
name = schema.TextLine(title=u"Contact name")
email = schema.TextLine(title=u"Email")
phone = schema.TextLine(title=u"Phone")
class IFacility(Interface):
name = schema.TextLine(title=u"Facility name")
contacts = schema.List(title=u"Contacts",
value_type=schema.Object(schema=IFacilityContact),
default=[])
I don't understand why that default=[] is required. If I render the form as a view it isn't, but when I render it as a viewlet (with a very slightly customised template) it is, otherwise I get this error:
- Expression: <PathExpr standard:u'widget'> TypeError: iteration over non-sequence
Anyway, on with the object definition:
class FacilityContact(Persistent):
grok.implements(IFacilityContact)
name = ''
phone = ''
email = ''
class Facility(grok.Container):
grok.implements(IFacility)
def __init__(self, name=''):
super(Facility, self).__init__()
self.name = name
self.contacts = PersistentList()
I wanted to use formlib to automatically generate the edit form. So, a standard grok edit form:
from zope.formlib import form
import zope.event
from zope.component import getMultiAdapter
from zope.lifecycleevent import ObjectModifiedEvent
class EditFacility(grok.Viewlet):
grok.viewletmanager(Edit)
grok.name("Edit facility details")
def update(self):
self.form = getMultiAdapter((self.context, self.request),
name='editfacilityform')
self.form.update_form()
def render(self):
return self.form.render()
class EditFacilityForm(grok.EditForm):
form_fields = grok.AutoFields(IFacility)
template = subpage_edit_form #A template without html headers for viewlets
@form.action("Apply", condition=form.haveInputWidgets)
def handle_edit_action(self, action, data):
if form.applyChanges(self.context, self.form_fields, data, self.adapters):
zope.event.notify(ObjectModifiedEvent(self.context))
self.request.response.redirect('.')
Now no widgets have been registered for Object yet, so we get a component lookup error:
ComponentLookupError: ((<zope.schema._field.Object object at 0x2c91530>, <zope.publisher.browser.BrowserRequest instance URL=http://localhost:8080/test2/edit>), <InterfaceClass zope.app.form.interfaces.IInputWidget>, u'')
Let's register an adapter for object. I can't remember where I stole this code from...
from zope.app import zapi
from zope.app.form.interfaces import IInputWidget
from zope.interface import implements
from zope.schema.interfaces import IObject
from zope.publisher.interfaces.browser import IBrowserRequest
import grok
class ObjectInputWidgetView(grok.MultiAdapter):
grok.adapts(IObject, IBrowserRequest)
grok.implements(IInputWidget)
def __new__(self, context, request):
"""Dispatch widget for Object schema field to a widget that is
registered for (IObject, schema, IBrowserRequest) where schema
is the schema of the object."""
class Obj(object):
implements(context.schema)
widget=zapi.getMultiAdapter((context, Obj(), request), IInputWidget)
return widget
This adapter looks up another adapter based on the context, the schema and the request. So we now need to register an adapter for our schema:
from zope.app.form.browser import ObjectWidget
class ContactWidget(grok.MultiAdapter):
grok.adapts(IObject, IFacilityContact, IBrowserRequest)
grok.implements(IInputWidget)
def __new__(self, context, obj, request):
return ObjectWidget(context, request, FacilityContact)
And now, things should finally work.
Comments
Leave a comment
Jesse wrote on 19 August 2008:
Grok expects 3 arguments for method __new__ of ContactWidget. I deleted "obj" then it seems to work.