# 31. Vocabularies, Registry-Settings and Control Panels¶

In this part you will:

• Store custom settings in the registry

• Create a controlpanel to manage custom settings

• Create options in fields as vocabularies

• Assign talks to rooms

Topics covered:

• plone.app.registry

• Vocabularies

• Control panels

## 31.1. Introduction¶

Do you remember the fields audience and type_of_talk from the talk content-type? We provided several options to chose from that were hard-coded in the schema.

Next we want to add a field to assign talks to a room. Since the conference next year will have different room numbers or names these values need to to be editable.

And while we're at it: It would be much better to have the options for audience and type_of_talk editable by admins as well, e.g. to be able to add Lightning Talks!

By combining the registry, a controlpanel and vocabularies you can allow rooms to be editable options.

To be able to to so you first need to get to know the registry.

## 31.2. 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 since 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 that you 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.

Note

This UI for the registry is not yet available in Volto.

## 31.3. Registry-records¶

You already added an additional criterion usable for Collections in profiles/default/registry/querystring.xml. This setting is stored in the registry.

Let's look at existing values in the registry

Go to http://localhost:3000/controlpanel/navigation and add talk to the field Displayed content types. After saving talks that are in the root-folder will show up in the navigation.

This setting is stored in plone.displayed_types.

## 31.4. Accessing and modifying records in the registry¶

In Python you can access the registry with this value like this:

1from plone.registry.interfaces import IRegistry
2from zope.component import getUtility
3
4registry = getUtility(IRegistry)
5displayed_types = registry.get('plone.displayed_types')


displayed_types is then the tuple ('Image', 'File', 'Link', 'News Item', 'Folder', 'Document', 'Event', 'talk')

plone.api holds convenience methods to make this even easier:

1from plone import api
2
3api.portal.get_registry_record('plone.displayed_types')
4api.portal.set_registry_record('plone.smtp_host', 'my.mail.server')


## 31.5. Managing custom registry records¶

Now let's add our own custom settings:

• Is talk submission open or closed?

• Which rooms are available for talks?

While we're at it we can also add new settings types_of_talk and audiences that we will use later for the fields type_of_talk and audience.

To define custom records you write the same type of schema as you already did for dexterity types or for behaviors:

Add a file browser/controlpanel.py:

 1from zope import schema
2from zope.interface import Interface
3
4
5class IPloneconfControlPanel(Interface):
6
7    talk_submission_open = schema.Bool(
8        title='Allow talk submission',
9        description='Allow the submission of talks for anonymous user',
10        default=False,
11        required=False,
12    )
13
14    types_of_talk = schema.List(
15        title=u'Available types for talks',
16        default=['Talk', 'Training', 'Keynote'],
17        missing_value=None,
18        required=False,
19        value_type=schema.TextLine(),
20    )
21
22    audiences = schema.List(
23        title='Available audiences for talks',
24        default=['Beginner', 'Advanced', 'Professional'],
25        missing_value=None,
26        required=False,
27        value_type=schema.TextLine(),
28    )
29
30    rooms = schema.Tuple(
31        title='Available Rooms for the conference',
32        default=('101', '201', 'Auditorium'),
33        missing_value=None,
34        required=False,
35        value_type=schema.TextLine(),
36    )


You now have to register this schema for the registry. Add the following to profiles/default/registry/main.xml

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


Note

The prefix allows you access these records with a shortcut: You can use ploneconf.rooms instead of having to use ploneconf.site.browser.controlpanel.IPloneconfControlPanel.room.

After reinstalling the package (to load the registry entry) you can access and modify these values in the registry as described above:

Either use http://localhost:8080/Plone/portal_registry or python:

from plone import api

api.portal.get_registry_record('ploneconf.rooms')


Note

We use python to define the values.

Alternatively you could also add these values only using Generic Setup.

You could even create new records through the web using http://localhost:8080/Plone/portal_registry.

The following creates a new value ploneconf.talk_submission_open using Generic Setup:

1<record name="ploneconf.talk_submission_open">
2  <field type="plone.registry.field.Bool">
3    <title>Allow talk submission</title>
4    <description>Allow the submission of talks for anonymous users</description>
5    <required>False</required>
6  </field>
7  <value>False</value>
8</record>


When creating a new site a lot of default settings are created that way. See https://github.com/plone/Products.CMFPlone/blob/master/Products/CMFPlone/profiles/dependencies/registry.xml to see how Products.CMFPlone registers values.

## 31.6. Add a custom control panel¶

Now you will add a custom control panel to edit all setting related to our package with a nice UI.

To register a controlpanel in Volto and Plone Classic you need quite a bit of boiler-plate:

 1from plone.app.registry.browser.controlpanel import ControlPanelFormWrapper
2from plone.app.registry.browser.controlpanel import RegistryEditForm
3from plone.restapi.controlpanels import RegistryConfigletPanel
4from plone.z3cform import layout
5from zope import schema
6from zope.component import adapter
7from zope.interface import Interface
8
9
10class IPloneconfControlPanel(Interface):
11
12    talk_submission_open = schema.Bool(
13        title=u'Allow talk submission',
14        description=u'Allow the submission of talks for anonymous user',
15        default=False,
16        required=False,
17    )
18
19    types_of_talk = schema.List(
20        title=u'Available types for talks',
21        default=[u'Talk', u'Training', u'Keynote', u'Lightning Talk'],
22        missing_value=None,
23        required=False,
24        value_type=schema.TextLine(),
25    )
26
27    audiences = schema.List(
28        title=u'Available audiences for talks',
29        default=[u'Beginner', u'Advanced', u'Professional'],
30        missing_value=None,
31        required=False,
32        value_type=schema.TextLine(),
33    )
34
35    rooms = schema.Tuple(
36        title=u'Available Rooms for the conference',
37        default=(u'101', u'201', u'Auditorium'),
38        missing_value=None,
39        required=False,
40        value_type=schema.TextLine(),
41    )
42
43
45class PloneconfControlPanel(RegistryConfigletPanel):
46    schema = IPloneconfControlPanel
47    schema_prefix = 'ploneconf'
48    configlet_id = 'ploneconf-controlpanel'
49    configlet_category_id = 'General'
50    title = 'Ploneconf Settings'
51    group = 'Products'
52
53
54class PloneconfControlPanelForm(RegistryEditForm):
55    schema = IPloneconfControlPanel
56    schema_prefix = 'ploneconf'
57    label = u'Ploneconf Settings'
58
59
60PloneconfControlPanelView = layout.wrap_form(
61    PloneconfControlPanelForm, ControlPanelFormWrapper)


You also need to register these in browser/configure.zcml:

 1<browser:page
2    name="ploneconf-controlpanel"
3    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
4    class=".controlpanel.PloneconfControlPanelView"
5    permission="cmf.ManagePortal"
6    />
7
9    factory="ploneconf.site.browser.controlpanel.PloneconfControlPanel"
10    name="ploneconf-controlpanel" />


Finally you also need to register it in Generic Setup. Add a file profiles/default/controlpanel.xml:

 1<?xml version="1.0"?>
2<object name="portal_controlpanel">
3  <configlet
4      title="Ploneconf Settings"
5      action_id="ploneconf-controlpanel"
6      appId="ploneconf-controlpanel"
7      category="Products"
8      condition_expr=""
9      icon_expr=""
10      url_expr="string:${portal_url}/@@ploneconf-controlpanel" 11 visible="True"> 12 <permission>Manage portal</permission> 13 </configlet> 14</object>  After applying the profile (e.g. by reinstall the package) your control panel shows up. In Volto it is at http://localhost:3000/controlpanel/ploneconf-controlpanel In Plone Classic at http://localhost:8080/Plone/ploneconf-controlpanel ## 31.7. Vocabularies¶ Now the custom settings are stored in the registry that we can modify then in a nice way as admins. We still need to use these options in talks. To do so we turn them into vocabularies. Vocabularies are often used for selection fields. They have many benefits: • They allow you to separate the displayed option and the stored value for a field. This allows translating titles while using the same values. • They can be created dynamically, so the available options can change depending on existing content, the role of the user or even the time of day. Create a file vocabularies.py and write code that generates vocabularies from these settings:  1from plone import api 2from plone.app.vocabularies.terms import safe_simplevocabulary_from_values 3from zope.interface import provider 4from zope.schema.interfaces import IVocabularyFactory 5 6 7@provider(IVocabularyFactory) 8def RoomsVocabularyFactory(context): 9 name = 'ploneconf.rooms' 10 values = api.portal.get_registry_record(name) 11 return safe_simplevocabulary_from_values(values) 12 13 14@provider(IVocabularyFactory) 15def TalkTypesVocabulary(context): 16 name = 'ploneconf.types_of_talk' 17 values = api.portal.get_registry_record(name) 18 return safe_simplevocabulary_from_values(values) 19 20 21@provider(IVocabularyFactory) 22def AudiencesVocabulary(context): 23 name = 'ploneconf.audiences' 24 values = api.portal.get_registry_record(name) 25 return safe_simplevocabulary_from_values(values)  You can now register these vocabularies as named utilities in configure.zcml: <utility name="ploneconf.types_of_talk" component="ploneconf.site.vocabularies.TalkTypesVocabulary" /> <utility name="ploneconf.audiences" component="ploneconf.site.vocabularies.AudiencesVocabulary" /> <utility name="ploneconf.rooms" component="ploneconf.site.vocabularies.RoomsVocabularyFactory" />  From now on you can use these vocabulary by referring to their name, e.g. ploneconf.rooms. Note • Plone comes with many useful named vocabularies that you can use in your own projects, for example plone.app.vocabularies.Users or plone.app.vocabularies.PortalTypes. • See https://github.com/plone/plone.app.vocabularies/ for a list of vocabularies. • 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 ASCII. • binascii.b2a_qp (which is used by safe_simplevocabulary_from_values) has the annoying habit of adding line-breaks every 80 characters. Make sure your values are shorter than that or use something else to create the vocabulary-terms! • 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], )  ## 31.8. Using vocabularies in a schema¶ To use a vocabulary in a schema replace values with vocabulary and point to the vocbulary by name: 1type_of_talk = schema.Choice( 2 title=_(u'Type of talk'), 3 vocabulary='ploneconf.types_of_talk', 4 required=True, 5)  Don't forget to add the new field room now. Edit content/talk.py:  1# -*- coding: utf-8 -*- 2from plone.app.textfield import RichText 3from plone.autoform import directives 4from plone.dexterity.content import Container 5from plone.namedfile.field import NamedBlobImage 6from plone.schema.email import Email 7from plone.supermodel import model 8from ploneconf.site import _ 9from z3c.form.browser.checkbox import CheckBoxFieldWidget 10from z3c.form.browser.radio import RadioFieldWidget 11from zope import schema 12from zope.interface import implementer 13from zope.schema.vocabulary import SimpleTerm 14from zope.schema.vocabulary import SimpleVocabulary 15 16 17class ITalk(model.Schema): 18 """Dexterity-Schema for Talks""" 19 20 directives.widget(type_of_talk=RadioFieldWidget) 21 type_of_talk = schema.Choice( 22 title=_(u'Type of talk'), 23 vocabulary='ploneconf.types_of_talk', 24 required=True, 25 ) 26 27 details = RichText( 28 title=_(u'Details'), 29 description=_(u'Description of the talk (max. 2000 characters)'), 30 max_length=2000, 31 required=True, 32 ) 33 34 directives.widget(audience=CheckBoxFieldWidget) 35 audience = schema.Set( 36 title=_(u'Audience'), 37 value_type=schema.Choice(vocabulary='ploneconf.audiences'), 38 required=False, 39 ) 40 41 speaker = schema.TextLine( 42 title=_(u'Speaker'), 43 description=_(u'Name (or names) of the speaker'), 44 required=False, 45 ) 46 47 company = schema.TextLine( 48 title=_(u'Company'), 49 required=False, 50 ) 51 52 email = Email( 53 title=_(u'Email'), 54 description=_(u'Email adress of the speaker'), 55 required=False, 56 ) 57 58 website = schema.TextLine( 59 title=_(u'Website'), 60 required=False, 61 ) 62 63 twitter = schema.TextLine( 64 title=_(u'Twitter name'), 65 required=False, 66 ) 67 68 github = schema.TextLine( 69 title=_(u'Github username'), 70 required=False, 71 ) 72 73 image = NamedBlobImage( 74 title=_(u'Image'), 75 description=_(u'Portrait of the speaker'), 76 required=False, 77 ) 78 79 speaker_biography = RichText( 80 title=_(u'Speaker Biography (max. 1000 characters)'), 81 max_length=1000, 82 required=False, 83 ) 84 85 directives.widget(room=CheckBoxFieldWidget) 86 room = schema.Set( 87 title=_(u'Room'), 88 value_type=schema.Choice(vocabulary='ploneconf.rooms'), 89 required=False, 90 ) 91 92 93@implementer(ITalk) 94class Talk(Container): 95 """Talk instance class"""  One tiny thing is still missing: We should display the room. Modify frontend/src/components/Views/Talk.jsx an add this after the When component: 1 {content.room && ( 2 <> 3 <Header dividing sub> 4 Where 5 </Header> 6 <p>{content.room.title}</p> 7 </> 8 )}  The complete TalkView import React from 'react'; import { flattenToAppURL } from '@plone/volto/helpers'; import { Container, Header, Image, Icon, Label, Segment, } from 'semantic-ui-react'; import { Helmet } from '@plone/volto/helpers'; import { When } from '@plone/volto/components/theme/View/EventDatesInfo'; const TalkView = (props) => { const { content } = props; const color_mapping = { Beginner: 'green', Advanced: 'yellow', Professional: 'red', }; return ( <Container id="page-talk"> <Helmet title={content.title} /> <h1 className="documentFirstHeading"> {content.type_of_talk.title}: {content.title} </h1> <Segment floated="right"> {content.start && !content.hide_date && ( <> <Header dividing sub> When </Header> <When start={content.start} end={content.end} whole_day={content.whole_day} open_end={content.open_end} /> </> )} {content.room && ( <> <Header dividing sub> Where </Header> <p>{content.room.title}</p> </> )} {content.audience && ( <Header dividing sub> Audience </Header> )} {content.audience.map((item) => { let audience = item.title; let color = color_mapping[audience] || 'green'; return ( <Label key={audience} color={color}> {audience} </Label> ); })} </Segment> {content.description && ( <p className="documentDescription">{content.description}</p> )} {content.details && ( <div dangerouslySetInnerHTML={{ __html: content.details.data }} /> )} {content.speaker && ( <Segment clearing> <Header dividing>{content.speaker}</Header> {content.website ? ( <p> <a href={content.website}>{content.company}</a> </p> ) : ( <p>{content.company}</p> )} {content.email && ( <p> Email: <a href={mailto:${content.email}}>{content.email}</a>
</p>
)}
<p>
<a href={https://twitter.com/${content.twitter}}> {content.twitter.startsWith('@') ? content.twitter : '@' + content.twitter} </a> </p> )} {content.github && ( <p> Github:{' '} <a href={https://github.com/${content.github}}>
{content.github}
</a>
</p>
)}
{content.image && (
<Image
size="small"
floated="right"
alt={content.image_caption}
avatar
/>
)}
{content.speaker_biography && (
<div
dangerouslySetInnerHTML={{
__html: content.speaker_biography.data,
}}
/>
)}
</Segment>
)}
</Container>
);
};
export default TalkView;


By the way: When using a vocabulary you can also drop the annoying item.title || item.token pattern.

Note

The approach to create options for fields from registry-records has one problem: Existing talks are not updated when you change a value in the controlpanel. Instead they will have invalid data and you will have to update them.

If the options in your fields tend to change often you should consider using collective.taxonomy to manage vocabularies. Among many other things it allows you to translate terms and to change the text that is displayed while keeping the values the same. Using collective.taxonomy for vocabularies works fine with Volto, but the UI where you create and edit vocabularies is so far only available in Plone Classic.

In this case study the approach used here works fine though because you will create a new site for next years conference anyway.

## 31.9. Summary¶

• You successfully combined the registry, a controlpanel and vocabularies to allow managing field options by admins.

• It seems like a lot but you will certainly use dynamic vocabularies, controlpanels and the registry in all of your future Plone projects in one way or another.