Adding configuration settings using Zope 3 schemas and formlib

This how-to explains how to add a configlet to Plone's control panel and letting the Zope 3 framework do the work for you.

We're going to put together three components. A local utility where we store the settings, a schema that defines the settings and a view that takes care of the rendering. We glue together the view and the storage by registering a simple adapter.

First, the components

Let's first define the fields that make up our configuration settings. We use zope.schema for this. As part of best practices in Zope development, we support internationalization by wrapping all strings as messages.

Important: If you're using Plone 2.5 you must make sure that your site is registered as a Five local site. That's beyond the scope of this how-to, but you can look over the shoulders of Rocky Burt in the Plone4Artists project. Specifically the p4a.common module.

interfaces.py:
from zope.interface import Interface
from zope import schema
from zope.i18nmessageid import MessageFactory

_ = MessageFactory('some_message_domain')

class ISillyConfiguration(Interface):
  """This interface defines the configlet."""

  favorite_color = schema.TextLine(title=_(u"Enter your favorite color"),
                                  required=True) 

Next we'll set up the configlet view class:

browser/config.py:
from zope.formlib import form
from zope.i18nmessageid import MessageFactory

from Products.Five.formlib import formbase

from interfaces import ISillyConfiguration

_ = MessageFactory('some_message_domain')

class SillyConfigurationForm(formbase.EditFormBase):
    form_fields = form.Fields(ISillyConfiguration)

    label = _(u"A silly settings form")

We need to register this view on the Plone site. Here's the required configuration:

browser/configuration.zcml:
<configure 
    xmlns="http://namespaces.zope.org/zope"
    xmlns:browser="http://namespaces.zope.org/browser">

  <browser:page
     for="Products.CMFPlone.Portal.PloneSite"
     name="silly-configuration"
     class=".config.SillyConfigurationForm"
     permission="cmf.ManagePortal"
     />

</configure>

We don't want to store the settings on the Plone site itself. Let's add a local utility and store it there:

config.py:
from zope.interface import implements
from zope.schema.fieldproperty import FieldProperty

from interfaces import ISillyConfiguration

from OFS.SimpleItem import SimpleItem

class SillyConfiguration(SimpleItem):
    implements(ISillyConfiguration)
    
    contact_email = FieldProperty(ISillyConfiguration['contact_email'])

We'll create and register this local utility when we install our product. Make sure the following method is called as part of the normal product installation routine.

sitesetup.py:
from Products.SillyProduct.interfaces import ISillyConfiguration
from Products.SillyProduct.config import SillyConfiguration

def setup_site(portal):
  sm = portal.getSiteManager()

  if not sm.queryUtility(interfaces.ISillyConfiguration, name='silly_config'):
    sm.registerUtility(SillyConfiguration(),
                       interfaces.ISillyConfiguration,
                       'silly_config')

Note that if you're on Zope 2.9 you need to reverse the first two arguments for the registerUtility call.

Add glue

All that's left now is to add an adapter that tells the configuration form that we it should use our local utility to store the schema attributes.

Instead of registering a class to adapt our components we just use a normal function. The way it works is that formlib will look for an adapter to tell it how to glue together the fields with the context and it uses the resulting object to write the fields to. Since we just want to store the settings in the local utility we look it up and return it.

configure.zcml:
<adapter
   for="Products.CMFPlone.Portal.PloneSite"
   provides=".interfaces.ISillyConfiguration"
   factory=".config.form_adapter" />
config.py:
from zope.component import getUtility

from interfaces import ISillyConfiguration

def form_adapter(context):
    return getUtility(ISillyConfiguration, name='silly_config', context=context)

What's left

You'll probably want to add your new configlet to the control panel by adding it to your controlpanel.xml (see CMFPlone/profiles/default/controlpanel.xml). You can define the icon by adding it to actionicons.xml.

error in call of `registerUtility`

Posted by Tom Lazar at Aug 25, 2007 11:58 PM
Malthe, thanks for this tutorial. just what i needed. however, there's one error: you got the order of parameters wrong for `registerUtility`:

instead of `sm.registerUtility(interfaces.ISillyConfiguration, SillyConfiguration(), 'silly_config')` it needs to be `sm.registerUtility(SillyConfiguration(), ISillyConfiguration,'silly_config')`

re: error in call to registerUtility

Posted by Malthe Borch at Aug 26, 2007 01:09 AM
I've changed it Zope 2.10 syntax and made a note for 2.9 users. Thanks!

named parameters?

Posted by Tom Lazar at Aug 26, 2007 02:58 AM
why not just use named parameters, then the order should be of no consequence.

no can do

Posted by Malthe Borch at Aug 26, 2007 11:52 AM
def registerUtility(component, provided=None, name=u'', info=u'') vs.
def registerUtility(interface, utility, name='')

gotcha ;-)

Posted by Tom Lazar at Aug 26, 2007 05:24 PM
next time i'll look it up first... ;)

use plone.app.controlpanel.form.ControlPanelForm

Posted by Tom Lazar at Aug 26, 2007 06:45 PM
If you use Products.Five.formlib.formbase.EditFormBase as base class for the view you end up with a 'vanilla' webform without any plone styles or menus. Instead base it on plone.app.controlpanel.form.ControlPanelForm (but make sure to add a `description` and a `form_name` field, as well, as they're mandatory).

you then have nice controlpanel, just like all the other ones ;)

use `category="Products"` in controlpanel.xml

Posted by Tom Lazar at Aug 26, 2007 09:06 PM
Then your control panel won't be grouped into the default panels but into a separate 'add-on' section.

Register Utility In configure.zcml

Posted by Nathan Van Gheem at Jun 05, 2008 10:58 PM
It is probably better to do it in the configuration zcml like so,

<utility
        provides=".interfaces.ISillyConfiguration"
        factory=".configlet.SillyConfiguration"
        name="sillyconfig-name"
        />

Is not a local utility

Posted by Jan Murre at Jul 02, 2008 07:30 PM
This type of configuration does not work for me. You need a local utility that can only be registered through python code.

Data Not Persisting

Posted by Nathan Van Gheem at Jun 12, 2008 10:38 PM
For some reason with this implementation I cannot get the data to persist on product reinstalls. Is there any way around this?

It's the quickinstaller

Posted by Jan Murre at Jul 02, 2008 07:33 PM
The quickinstaller keeps track of installed utilities. On re-install they are removed and added again.
So you lose any persistent data.

passing context is redundant for getUtility

Posted by Kapil Thangavelu at Jul 10, 2008 05:58 PM
local site semantics determine the nearest component registry used for lookup of components. in fact passing context is arguably an error as it will fail if the context is not a site. where as it will work for any context if context is not passed by looking up the nearest component registry, else the context must implement/adapt to IComponentLookup.

Use ControlPanelForm instead of EditFormBase

Posted by Goldmund, Wyldebeast & Wunderliebe at Dec 01, 2008 12:18 PM
Nowadays it is even nicer to use ControlPanelForm as base class for SillyConfigurationForm.
This gives you the link back to main configuration, and the configlets portlet. It goes like so:

from plone.app.controlpanel.form import ControlPanelForm

 ...

class SillyConfigurationForm(ControlPanelForm):

    form_fields = form.Fields(ISillyConfiguration)
    label = _(u"A silly settings form")
    description = _(u"Silly description")
    form_name = _(u"Silly Settings")

... and remember to indent correctly

Posted by Goldmund, Wyldebeast & Wunderliebe at Dec 01, 2008 12:20 PM
Sorry, indentation got lost in the previous comment. Well, you'll know what it should be, don't you?

use sm.getSite() in plone 3.1.x

Posted by Mathias Leigmruber at Feb 04, 2009 03:03 AM
getSiteManager is deprecated in plone 3.1.x (used in a plone 3.1.7 environment)

...
sm.getSite()
...

import of ISillyConfiguration in browser/config.py

Posted by Kees Hink (Goldmund, Wyldebeast & Wunderliebe) at Mar 10, 2009 09:48 AM
In browser/config.py, there's an incorrect import statement: 'from interfaces import ISillyConfiguration' ought to be 'from Products.SillyProduct.interfaces import ISillyConfiguration'