Object lists with Grok and formlib

16 June 2008 15:10

Today 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

Jesse wrote on 19 August 2008:

Grok expects 3 arguments for method __new__ of ContactWidget. I deleted "obj" then it seems to work.

Leave a comment