38. Manage Settings with Registry, Control Panels and Vocabularies#

In this part you will:

  • Store a custom setting in a registry

  • Create a control panel using z3c.form to allow setting that value

Topics covered:

  • plone.app.registry

  • control panels

38.1. The Registry#

The registry is used to get and set values stored in records. Each record contains the actual value, as well as a field that describes the record in more detail. It has a nice dict-like API.

All global settings in Plone 5 are stored in the registry.

The registry itself is provided by plone.registry and the UI to interact with it by plone.app.registry

Almost all settings in /plone_control_panel are actually stored in the registry and can be modified using its UI directly.

Open http://localhost:8080/Plone/portal_registry and filter for displayed_types. You see can modify the content types that should be shown in the navigation and site map. The values are the same as in http://localhost:8080/Plone/@@navigation-controlpanel but the later form is customized for usability.

38.2. A setting#

Let's store two values in the registry:

  • The date of the conference

  • Is talk submission open or closed

You cannot create values through the web; instead, you need to register them using Generic Setup.

Open the file profiles/default/registry.xml. You already registered several new settings in there:

  • You enabled self registration

  • You stored a site logo

  • You registered additional criteria usable for Collections

Adding the following code to registry.xml. This creates a new value in the registry upon installation of the package.

<record name="ploneconf.talk_submission_open">
  <field type="plone.registry.field.Bool">
    <title>Allow talk submission</title>
    <description>Allow the submission of talks for anonymous users</description>
    <required>False</required>
  </field>
  <value>False</value>
</record>

When creating a new site a lot of settings are created in the same way. See plone/Products.CMFPlone to see how Products.CMFPlone registers values.

<record name="ploneconf.date_of_conference">
  <field type="plone.registry.field.Date">
    <title>First day of the conference</title>
    <required>False</required>
  </field>
  <value>2016-10-17</value>
</record>

38.3. Accessing and modifying values in the registry#

In Python you can access the registry like this:

from plone.registry.interfaces import IRegistry
from zope.component import getUtility

registry = getUtility(IRegistry)
start = registry.get('ploneconf.date_of_conference')

plone.api holds methods to make this even easier:

from plone import api
api.portal.get_registry_record('ploneconf.date_of_conference')
api.portal.set_registry_record('ploneconf.talk_submission_open', True)

38.4. Add a custom control panel#

When you want to add a custom control panel it is usually more convenient to register the fields, not manually as above, but as fields in a schema, similar to that of a content type schema.

For this you define an interface for the schema and a view that auto-generates a form from the schema. In browser/configure.zcml add:

<browser:page
    name="ploneconf-controlpanel"
    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
    class=".controlpanel.PloneconfControlPanelView"
    permission="cmf.ManagePortal"
    />

Add a file browser/controlpanel.py:

# -*- coding: utf-8 -*-
from datetime import date
from plone.app.registry.browser.controlpanel import ControlPanelFormWrapper
from plone.app.registry.browser.controlpanel import RegistryEditForm
from plone.z3cform import layout
from zope import schema
from zope.interface import Interface


class IPloneconfControlPanel(Interface):

    date_of_conference = schema.Date(
        title=u'First day of the conference',
        required=False,
        default=date(2016, 10, 17),
    )

    talk_submission_open = schema.Bool(
        title=u'Allow talk submission',
        description=u'Allow the submission of talks for anonymous user',
        default=False,
        required=False,
    )


class PloneconfControlPanelForm(RegistryEditForm):
    schema = IPloneconfControlPanel
    schema_prefix = "ploneconf"
    label = u'Ploneconf Settings'


PloneconfControlPanelView = layout.wrap_form(
    PloneconfControlPanelForm, ControlPanelFormWrapper)

With this way of using fields you don't have to register the values in registry.xml. Instead, you have to register the interface:

<records interface="ploneconf.site.browser.controlpanel.IPloneconfControlPanel"
         prefix="ploneconf" />

After reinstalling the package (to load the registry entry) you can access the control panel at http://localhost:8080/Plone/@@ploneconf-controlpanel.

To make it show up in the general control panel at http://localhost:8080/Plone/@@overview-controlpanel you have to register it with GenericSetup. Add a file profiles/default/controlpanel.xml:

<?xml version="1.0"?>
<object name="portal_controlpanel">
  <configlet
      title="Ploneconf Settings"
      action_id="ploneconf-controlpanel"
      appId="ploneconf-controlpanel"
      category="Products"
      condition_expr=""
      icon_expr=""
      url_expr="string:${portal_url}/@@ploneconf-controlpanel"
      visible="True">
    <permission>Manage portal</permission>
  </configlet>
</object>

Again, after applying the profile (reinstall the package or write a upgrade-step) your control panel shows up in http://localhost:8080/Plone/@@overview-controlpanel.

38.5. Vocabularies#

Do you remember the field rooms? We provided several options to chose from. But who says that the next conference will have the same rooms? These values should be configurable by the admin. The admin could go to the Dexterity control panel and change the values but we will use a different approach. We will allow the rooms to be added in the control panel and use these values in the talk-schema by registering a vocabulary.

Add a new field to IPloneconfControlPanel:

1rooms = schema.Tuple(
2    title=u'Available Rooms for the conference',
3    default=(u'101', u'201', u'Auditorium'),
4    missing_value=None,
5    required=False,
6    value_type=schema.TextLine(),
7)

Create a file vocabularies.py and write the vocabulary:

 1# -*- coding: utf-8 -*-
 2from plone import api
 3from plone.app.vocabularies.terms import safe_simplevocabulary_from_values
 4from zope.interface import provider
 5from zope.schema.interfaces import IVocabularyFactory
 6
 7@provider(IVocabularyFactory)
 8def RoomsVocabularyFactory(context):
 9    values = api.portal.get_registry_record('ploneconf.rooms')
10    return safe_simplevocabulary_from_values(values)

You can now register this vocabulary as a named utility in configure.zcml as ploneconf.site.vocabularies.Rooms:

<utility
    name="ploneconf.site.vocabularies.Rooms"
    component="ploneconf.site.vocabularies.RoomsVocabularyFactory" />

From now on you can use this vocabulary by only referring to its name ploneconf.site.vocabularies.Rooms.

Note:

  • Plone comes with many useful vocabularies that you can use in your own projects. See plone/plone.app.vocabularies for a list of them.

  • We turn the values from the registry into a dynamic SimpleVocabulary that can be used in the schema.

  • You could use the context with which the vocabulary is called or the request (using getRequest from from zope.globalrequest import getRequest) to constrain the values in the vocabulary.

  • We use the handy helper method safe_simplevocabulary_from_values to create the vocabulary since the token of a SimpleTerm in a SimpleVocabulary needs to be bytes, not unicode.

  • You can write your own helper to further control the creation of the vocabulary terms. The value is stored on the object, the token used to communicate with the widget during editing and title is what is displayed in the widget. This example allows you to translate the displayed title while keeping the value stored on the object the same in all languages:

    from binascii import b2a_qp
    from ploneconf.site import _
    from zope.schema.vocabulary import SimpleTerm
    from zope.schema.vocabulary import SimpleVocabulary
    
    def simplevoc(values):
        return SimpleVocabulary(
            [SimpleTerm(value=i, token=b2a_qp(i.encode('utf-8')), title=_(i)) for i in values],
        )
    

Use the new vocabulary in the talk schema. Edit content/talk.xml

1<field name="room"
2       type="zope.schema.Choice"
3       form:widget="z3c.form.browser.radio.RadioFieldWidget"
4       security:write-permission="cmf.ReviewPortalContent">
5  <description></description>
6  <title>Room</title>
7  <vocabulary>ploneconf.site.vocabularies.Rooms</vocabulary>
8</field>

In a Python schema, that would look like this:

directives.widget(room=RadioFieldWidget)
room = schema.Choice(
    title=_(u'Room'),
    vocabulary='ploneconf.site.vocabularies.Rooms',
    required=False,
)

An admin can now configure the rooms available for the conference.

We could use the same pattern for the fields type_of_talk and audience.