You are here: Home / What do you need? / Help and documentation / Plone Info / dynamic vocabularies, control panel configlets and plone.app.registry

dynamic vocabularies, control panel configlets and plone.app.registry

by Darrell Kingsley last modified Mar 13, 2014 01:00 PM
Describes how to set up a configlet in site settings to allow managers to maintain persistent configuration data for products. Specific case deals with maintaining lists of values to provide dynamic vocabularies for selection drop-down (Choice) fields in the Member data schema.

plone.app.registry provides ui integration for plone.registry which in turn provides the local utility (per site) data store and interfaces. They are core in plone 4. NB this only appears to be true for a root zeocluster install, and not for a rootless standalone install. I don't know whether it is the root, or the zeo which is important, or both.

This documentation is based on plone.org/documentation/kb/how-to-create-a-plone-control-panel-with-plone.app.registry

However this casued a ComponentLookupError at startup when getting the registry utility similar to the one described at www.mentby.com/Group/plone-users/dynamic-vocabulary-for-userdata-field-plone-4 - the reason for this is explained better atplone.293351.n2.nabble.com/Local-utilities-ComponentLookupError-td3049621. Essentially as the registry is a local utility the python startup code to create schemas for products does not have the context of the registry as it is local to the plone site and is not available at top-level Zope. This would not be a problem if we are just accessing configuration data in standard product scripts but is an issue for start-up glue (in my case userdataschema.py). The solution is mentioned in the first link www.mentby.com/Group/plone-users/dynamic-vocabulary-for-userdata-field-plone-4 - use grok (or five.grok for Plone 4.0.5 and Zope.2.12) to provide a special interface IContextSourceBinder which is documented atplone.org/products/dexterity/documentation/manual/schema-driven-forms/customising-form-behaviour/vocabularies. This interface allows the context of the form to be passed to the registry access code which is hidden in a method at run-time. Once this was done it just worked. Other Zope-based frameworks use it to. For example in the Silva CMS documentation on Zope Schema fields http://docs.infrae.com/silva/2.3/contents/fields.html#defining-a-context-dependent-zope-vocabulary

To create the registry and control panel form:

A walk through plone.org/documentation/kb/how-to-create-a-plone-control-panel-with-plone.app.registry ignoring some bits that just weren't needed as we already had plone.app.registry and plone.registry available. But see note above about zeo/root install.

1) Define the fields that make up the control panel form by creating a zope schema interface in our products interfaces.py...

...
class IEnhancedUserDataSettings(interface.Interface):
    """ Global settings. Definition of plone.registry fields to be maintained in Control Panel.
    """
    profession_vocabulary = schema.List(title=u"Professions drop-down",
        value_type=schema.TextLine(required=True),
        required=True)

2) Set up the control panel form and the view it uses by using the same interface in controlpanel.py...

from bb.nhs.userdata.browser.interfaces import IEnhancedUserDataSettings
from plone.app.registry.browser import controlpanel
class EnhancedUserDataSettingsEditForm(controlpanel.RegistryEditForm):
    schema = IEnhancedUserDataSettings
    label = u"Enhanced User Data settings"
    description = u"Drop-down field values"
    def updateFields(self):
        super(EnhancedUserDataSettingsEditForm, self).updateFields()
    def updateWidgets(self):
        super(EnhancedUserDataSettingsEditForm, self).updateWidgets()
class EnhancedUserDataSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
    form = EnhancedUserDataSettingsEditForm
 

3) Register this control panel view in the configure.zcml slug...

    <!-- Control panel -->
    <browser:page
        name="userdata-settings"
        for="Products.CMFPlone.interfaces.IPloneSiteRoot"
        class=".controlpanel.EnhancedUserDataSettingsControlPanel"
        permission="cmf.ManagePortal"
        />

4) Define a configlet to use this view in yout products control panel using generic setup - profiles/default/controlpanel.xml...

<?xml version="1.0"?>
<object
    name="portal_controlpanel"
    xmlns:i18n="http://xml.zope.org/namespaces/i18n"
    i18n:domain="bb.nhs.userdata"
    purge="False">
    <configlet
        title="NHS Userdata"
        action_id="nhs_userdata"
        appId="bb.nhs.userdata"
        category="Products"
        condition_expr=""
        url_expr="string:${portal_url}/@@userdata-settings"
        visible="True"
        i18n:attributes="title">
            <permission>Manage portal</permission>
    </configlet>
</object>

5) Tell generic setup to use plone.app.registry in profiles/default/metadata.xml...

<?xml version="1.0"?>
<metadata>
  <version>1.0</version>
  <dependencies>
  <dependency>profile-plone.app.registry:default</dependency>
  </dependencies>
</metadata>

6) Now define the registry store itself in profiles/default/registry.xml. Again this can be fast-tracked by using the schema interface...

<?xml version="1.0"?>
<registry>
 <records interface="bb.nhs.userdata.browser.interfaces.IEnhancedUserDataSettings" />
</registry>

You should now have a working control panel configlet usable by managers.

five.grok 

More about five.grok atplone.org/products/dexterity/documentation/manual/five.grok/referencemanual-all-pages

Probably only need to do this if using registry in product start-up code to avoid the ComponentLookupError described above, though most likely it'll have other uses too.

As we wanted to access the registry to provide vocabularies at run-time for drop-downs in userdataschema.py (and avoid the ComponentLookupError) five.grok was installed using the pinned versions detailed at pypi.python.org/pypi/five.grok for five.grok 1.2.0 which is for Zope 2.12. 

1) Install five.grok

In buildout.cfg you'll need...

eggs = 
...
    five.grok==1.2.0
...

which wasn't mentioned in instructions and to be safe also added the version pinning too...

[versions]
...
# grok framework
# five.grok = 1.2.0
grokcore.annotation = 1.2
grokcore.component = 1.8
grokcore.formlib = 1.5
grokcore.security = 1.4
grokcore.site = 1.2
grokcore.view = 1.12.2
grokcore.viewlet = 1.4.1
five.localsitemanager = 2.0.3
martian = 0.11.2
...

2) Now we can make a one-off source using a grok context source binder to do its magic in userdataschema.py. Firstly by defining a method to return the vocabulary that uses the context of the form by way of this special IContextSourceBinder interface... 

...
from five import grok
from zope.component import queryUtility
from zope.schema.interfaces import IContextSourceBinder
from zope.schema.vocabulary import SimpleVocabulary
from plone.registry.interfaces import IRegistry
from bb.nhs.userdata.browser.interfaces import IEnhancedUserDataSettings
@grok.provider(IContextSourceBinder)
def availableProfessions(context):
    registry = queryUtility(IRegistry)
    log.info("registry: %s" % registry)
    terms = []
    
    if registry is not None:
        settings = registry.forInterface(IEnhancedUserDataSettings)
        log.info("settings: %s" % settings)
        log.info("profession_vocabulary: %s" % settings.profession_vocabulary)
        for profession in settings.profession_vocabulary:
 #           # create a term - the arguments are the value, the token, and
  #          # the title (optional)
            terms.append(SimpleVocabulary.createTerm(profession, profession.encode('utf-8'), profession))
    
    return SimpleVocabulary(terms)
...

and further on using this vocabulary in the schema...

...
   profession = schema.Choice(source=availableProfessions,
        ...
        )
...

The documentation (link below) shows the use of schema.Set() wrapping shema.Choice() but couldn't get this to work (error on writing data) and schema.Choice worked. Maybe its for the parameterised and named versions below. Not checked this out yet.

plone.org/products/dexterity/documentation/manual/schema-driven-forms/customising-form-behaviour/vocabularies describes this in more detail as well as using parameterised sources (one method for multiple vocabularies whereas the one-off source described requires a method for each vocabulary stored in the registry) and named vocabularies for using distributed components. 

Accessing the registry

In the form used to select the values from the registry we access an external method so

<select name="profession" id="select_profession">
      <option value="">
         None specified
      </option>
      <tal:block tal:repeat="profession context/getProfessions">
         <option value="None"
           tal:attributes="value profession"
           tal:content="profession">
              profession value
         </option>
      </tal:block>
</select>

 

#!/usr/bin/python
def getProfessions(self):
    from zope.component import getUtility
    from plone.registry.interfaces import IRegistry
    registry = getUtility(IRegistry)
    from bb.nhs.userdata.browser.interfaces import IEnhancedUserDataSettings
    settings = registry.forInterface(IEnhancedUserDataSettings)
#    self.plone_log('Hello from me!')
    return(settings.profession_vocabulary)

Logging for Debugging

context.plone_log may well not work for component architecture scripts so logging may need setting up...

from logging import getLogger
log = getLogger('MARKER ')
...
log.info("message: %s" % variable)
...
Search the event.log(s) for 'MARKER' and bob's...