13. Content types: Reference – Mastering Plone 6 development

13. Content types: Reference#

This chapter documents common fields, widgets, directives that you can use with content types. Content types are often called dexterity types which refers to the rework of the content type concept by dexterity and abandoning the Archetypes system.

13.1. Fields included in Plone#

This is a schema with examples for all field-types that are shipped with Plone by default. They are arranged in fieldsets:

Text, boolean, email

Textline, RichText, Boolean, Email, URI

Number fields

Integer, Float

Date and time fields

Datetime, Date

Choice and Multiple Choice fields

Choice, List, Tuple, Set

Relation fields

Relationchoice, Relationlist

File fields

File, Image

See also

See the code in example.contenttype branch training

  1from plone.app.textfield import RichText
  2from plone.autoform import directives
  3from plone.dexterity.content import Container
  4
  5from plone.namedfile.field import NamedBlobFile
  6from plone.namedfile.field import NamedBlobImage
  7from plone.schema import Email
  8
  9from plone.supermodel import model
 10from plone.supermodel.directives import fieldset
 11from plone.supermodel.directives import primary
 12from z3c.relationfield.schema import RelationChoice
 13from z3c.relationfield.schema import RelationList
 14from zope import schema
 15from zope.interface import implementer
 16
 17
 18class IExample(model.Schema):
 19    """Dexterity-Schema with common field-types."""
 20
 21    # fieldset(
 22    #     "default",
 23    #     label="Text, Boolean, Email",
 24    #     fields=(
 25    #         "title",
 26    #         "description",
 27    #         "richtext_field",
 28    #         "bool_field",
 29    #         "email_field",
 30    #         "uri_field",
 31    #     ),
 32    # )
 33
 34    fieldset(
 35        "numberfields",
 36        label="Number",
 37        fields=("int_field", "float_field"),
 38    )
 39
 40    fieldset(
 41        "datetimefields",
 42        label="Date and time",
 43        fields=(
 44            "datetime_field",
 45            "date_field",
 46        ),
 47    )
 48
 49    fieldset(
 50        "choicefields",
 51        label="Choice",
 52        fields=(
 53            "choice_field",
 54            "list_field",
 55            "tuple_field",
 56            "set_field",
 57        ),
 58    )
 59
 60    fieldset(
 61        "relationfields_volto",
 62        label="Relation fields – Volto",
 63        fields=(
 64            "relationchoice_field_named_staticcatalogvocabulary",
 65            "relationlist_field_named_staticcatalogvocabulary",
 66        ),
 67    )
 68
 69    fieldset(
 70        "filefields",
 71        label="File",
 72        fields=("file_field", "image_field"),
 73    )
 74
 75    # Default fields
 76    primary("title")
 77    title = schema.TextLine(
 78        title="Primary Field (Textline)",
 79        description="zope.schema.TextLine",
 80        required=True,
 81    )
 82
 83    description = schema.TextLine(
 84        title="Description (Textline)",
 85        description="zope.schema.TextLine",
 86        required=False,
 87    )
 88
 89    # text_field = schema.Text(
 90    #     title="Text Field",
 91    #     description="zope.schema.Text",
 92    #     required=False,
 93    #     missing_value="",
 94    #     default="",
 95    # )
 96
 97    # textline_field = schema.TextLine(
 98    #     title="Textline field",
 99    #     description="A simple input field (zope.schema.TextLine)",
100    #     required=False,
101    # )
102
103    richtext_field = RichText(
104        title="RichText field",
105        description="This uses a richtext editor. (plone.app.textfield.RichText)",
106        max_length=2000,
107        required=False,
108    )
109
110    bool_field = schema.Bool(
111        title="Boolean field",
112        description="zope.schema.Bool",
113        required=False,
114    )
115
116    email_field = Email(
117        title="Email field",
118        description="A simple input field for a email (plone.schema.email.Email)",
119        required=False,
120    )
121
122    uri_field = schema.URI(
123        title="URI field",
124        description="A simple input field for a URLs (zope.schema.URI)",
125        required=False,
126    )
127
128    # Choice fields
129    choice_field = schema.Choice(
130        title="Choice field",
131        description="zope.schema.Choice",
132        values=["One", "Two", "Three"],
133        required=False,
134    )
135
136    list_field = schema.List(
137        title="List field",
138        description="zope.schema.List",
139        value_type=schema.Choice(
140            values=["Beginner", "Advanced", "Professional"],
141        ),
142        required=False,
143        missing_value=[],
144        default=[],
145    )
146
147    tuple_field = schema.Tuple(
148        title="Tuple field",
149        description="zope.schema.Tuple",
150        value_type=schema.Choice(
151            values=["Beginner", "Advanced", "Professional"],
152        ),
153        required=False,
154        missing_value=(),
155        default=(),
156    )
157
158    set_field = schema.Set(
159        title="Set field",
160        description="zope.schema.Set",
161        value_type=schema.Choice(
162            values=["Beginner", "Advanced", "Professional"],
163        ),
164        required=False,
165        missing_value=set(),
166        default=set(),
167    )
168
169    # File and image fields
170    image_field = NamedBlobImage(
171        title="Image field",
172        description="A upload field for images (plone.namedfile.field.NamedBlobImage)",
173        required=False,
174    )
175
176    file_field = NamedBlobFile(
177        title="File field",
178        description="A upload field for files (plone.namedfile.field.NamedBlobFile)",
179        required=False,
180    )
181
182    # Date and Time fields
183    datetime_field = schema.Datetime(
184        title="Datetime field",
185        description="Uses a date and time picker (zope.schema.Datetime)",
186        required=False,
187    )
188
189    date_field = schema.Date(
190        title="Date field",
191        description="Uses a date picker (zope.schema.Date)",
192        required=False,
193    )
194
195    """Relation fields like Volto likes it
196
197    RelationChoice and RelationList with named StaticCatalogVocabulary
198
199    StaticCatalogVocabulary registered with same name as field/relation.
200    This allowes Volto relations control panel to restrict potential targets.
201    """
202
203    relationchoice_field_named_staticcatalogvocabulary = RelationChoice(
204        title="RelationChoice – named StaticCatalogVocabulary – Select widget",
205        description="field/relation: relationchoice_field_named_staticcatalogvocabulary",
206        vocabulary="relationchoice_field_named_staticcatalogvocabulary",
207        required=False,
208    )
209    directives.widget(
210        "relationchoice_field_named_staticcatalogvocabulary",
211        frontendOptions={
212            "widget": "select",
213        },
214    )
215
216    relationlist_field_named_staticcatalogvocabulary = RelationList(
217        title="RelationList – named StaticCatalogVocabulary – Select widget",
218        description="field/relation: relationlist_field_named_staticcatalogvocabulary",
219        value_type=RelationChoice(
220            vocabulary="relationlist_field_named_staticcatalogvocabulary",
221        ),
222        required=False,
223        default=[],
224        missing_value=[],
225    )
226    directives.widget(
227        "relationlist_field_named_staticcatalogvocabulary",
228        frontendOptions={
229            "widget": "select",
230        },
231    )
232
233    # Number fields
234    int_field = schema.Int(
235        title="Integer Field (e.g. 12)",
236        description="zope.schema.Int",
237        required=False,
238    )
239
240    float_field = schema.Float(
241        title="Float field, e.g. 12.7",
242        description="zope.schema.Float",
243        required=False,
244    )
245
246
247@implementer(IExample)
248class Example(Container):
249    """Example instance class"""

13.2. How fields look like#

This is how these fields look like when editing content in Volto:

Default fields

Text and boolean fields#

Number fields

Number fields#

Date and time fields

Date and time fields#

Choice and multiple choice fields

Choice and multiple choice fields#

Reference fields

Reference fields#

File fields

File fields#

13.3. mixedfield (datagrid field)#

The mixedfield empowers your user to create a list of objects of mixed value types sharing the same schema. If you are familiar with the Plone Classic datagrid field this is the complementary field / widget combo for Plone. mixedfield is a combination of a Plone Classic JSONField and a widget for Plone. Nothing new, just a term to talk about linking backend and frontend.

Example is a custom history:

view mixedfield values

Backend#

Add a JSONField field to your content type schema.

 1from plone.schema import JSONField
 2
 3MIXEDFIELD_SCHEMA = json.dumps(
 4    {
 5        'type': 'object',
 6        'properties': {'items': {'type': 'array', 'items': {'type': 'object', 'properties': {}}}},
 7    }
 8)
 9
10class IExample(model.Schema):
11    """Dexterity-Schema"""
12
13    fieldset(
14        'datagrid',
15        label='Datagrid field',
16        fields=(
17            # 'datagrid_field',
18            'mixed_field',
19            ),
20    )
21
22    primary('title')
23    title = schema.TextLine(
24        title='Primary Field (Textline)',
25        description='zope.schema.TextLine',
26        required=True,
27        )
28
29    description = schema.TextLine(
30        title='Description (Textline)',
31        description='zope.schema.TextLine',
32        required=False,
33        )
34
35    history_field = JSONField(
36        title='Mixedfield: datagrid field for Plone',
37        required=False,
38        schema=MIXEDFIELD_SCHEMA,
39        widget='history_widget',
40        default={'items': []},
41        missing_value={'items': []},
42        )

Frontend#

Provide a widget in your favorite add-on with a schema of elementary fields you need.

 1import ObjectListWidget from '@plone/volto/components/manage/Widgets/ObjectListWidget';
 2
 3const ItemSchema = {
 4    title: 'History-Entry',
 5    properties: {
 6        historydate: {
 7            title: 'Date',
 8            widget: 'date',
 9        },
10        historytopic: {
11            title: 'What',
12        },
13        historyversion: {
14            title: 'Version',
15        },
16        historyauthor: {
17            title: 'Who',
18        },
19    },
20    fieldsets: [
21        {
22            id: 'default',
23            title: 'History-Entry',
24            fields: [
25                'historydate',
26                'historytopic',
27                'historyversion',
28                'historyauthor',
29            ],
30        },
31    ],
32    required: [],
33};
34
35const HistoryWidget = (props) => {
36    return (
37        <ObjectListWidget
38            schema={ItemSchema}
39            {...props}
40            value={props.value?.items || props.default?.items || []}
41            onChange={(id, value) => props.onChange(id, { items: value })}
42        />
43    );
44};
45
46export default HistoryWidget;

Keeping this example as simple as possible we skipped the localization. Please see Volto documentation for details.

Register this widget for the backend field of your choice in your apps configuration config.js. The following config code registers the custom Plone HistoryWidget for Plone Classic fields with widget "history_widget".

 1import { HistoryWidget } from '@rohberg/voltotestsomevoltothings/components';
 2
 3// All your imports required for the config here BEFORE this line
 4import '@plone/volto/config';
 5
 6export default function applyConfig(config) {
 7    config.settings = {
 8        ...config.settings,
 9        supportedLanguages: ['en', 'de', 'it'],
10        defaultLanguage: 'en',
11    };
12    config.widgets.widget.history_widget = HistoryWidget;
13
14    return config;
15}

Please be sure to use plone.restapi version >= 7.3.0. If you cannot upgrade plone.restapi then a registration per field id instead of a registration per field widget name is needed.

export default function applyConfig(config) {
  config.widgets.id.history_field = HistoryWidget;
  return config;
}

The user can now edit the values of the new field history_field.

That's what you did to accomplish this:

  • You added a new field of type JSONField with widget "history_widget" and default schema to your content type schema.

  • You registered the custom Plone widget for widget name "history_widget".

edit mixedfield values

A view (ExampleView) of the content type integrates a component to display the values of the field history_field.

 1import React from 'react';
 2import moment from 'moment';
 3import { Container, Table } from 'semantic-ui-react';
 4
 5const MyHistory = ({ history }) => {
 6    return (
 7        _CLIENT__ && (
 8        <Table celled className="history_list">
 9            <Table.Header>
10            <Table.Row>
11                <Table.HeaderCell>Date</Table.HeaderCell>
12                <Table.HeaderCell>What</Table.HeaderCell>
13                <Table.HeaderCell>Version</Table.HeaderCell>
14                <Table.HeaderCell>Who</Table.HeaderCell>
15            </Table.Row>
16            </Table.Header>
17
18            <Table.Body>
19            {history?.items?.map((item) => (
20                <Table.Row>
21                <Table.Cell>
22                    {item.historydate && moment(item.historydate).format('L')}
23                </Table.Cell>
24                <Table.Cell>{item.historytopic}</Table.Cell>
25                <Table.Cell>{item.historyversion}</Table.Cell>
26                <Table.Cell>{item.historyauthor}</Table.Cell>
27                </Table.Row>
28            ))}
29            </Table.Body>
30        </Table>
31        )
32    );
33};
34
35const ExampleView = ({ content }) => {
36    return (
37        <Container>
38        <h2>I am an ExampleView</h2>
39        <h3>History</h3>
40        <MyHistory history={content.history_field} />
41        </Container>
42    );
43 };
44
45 export default ExampleView;

Et voilà.

view mixedfield values

13.4. Widgets#

Volto makes suggestions which widget to use, based on the fields type, backend widget and id.

All widgets are listed here: frontend widgets

Determine frontend widget#

If you want to register a frontend widget for your field, you can define your field such as:

directives.widget(
    "specialfield",
    frontendOptions={
        "widget": "specialwidget"
    })
specialfield = schema.TextLine(title="Field with special frontend widget")

Then register your frontend widget in your apps configuration.

import { MySpecialWidget } from './components';

const applyConfig = (config) => {
  config.widgets.widget.specialwidget = MySpecialWidget;
  return config;
}

You can also pass additional props to the frontend widget using the widgetProps key:

directives.widget(
    "specialfield",
    frontendOptions={
        "widget": "specialwidget",
        "widgetProps": {"isLarge": True, "color": "red"}
    })
specialfield = schema.TextLine(title="Field with special frontend widget")

The props will be injected into the corresponding widget component, configuring it as specified.

13.5. Directives#

Directives can be placed anywhere in the class body (annotations are made directly on the class). By convention they are kept next to the fields they apply to.

For example, here is a schema that omits a field:

from plone.autoform import directives
from plone.supermodel import model
from zope import schema


class ISampleSchema(model.Schema):

    title = schema.TextLine(title='Title')

    directives.omitted('additionalInfo')
    additionalInfo = schema.Bytes()

You can also handle multiple fields with one directive:

directives.omitted('field_1', 'field_2')

With the directive "mode" you can set fields to 'input', 'display' or 'hidden'.

directives.mode(additionalInfo='hidden')

You can apply directives to certain forms only. Here we drop a field from the add-form, it will still show up in the edit-form.

from z3c.form.interfaces import IAddForm

class ITask(model.Schema):

    title = schema.TextLine(title='Title')

    directives.omitted(IAddForm, 'done')
    done = schema.Bool(
        title='Done',
        required=False,
    )

The same works for custom forms.

With the directive widget() you can not only change the widget used for a field. With pattern_options you can pass additional parameters to the widget. Here, we configure the datetime widget powered by the JavaScript library pickadate by adding options that are used by it. Plone passes the options to the library.

class IMeeting(model.Schema):

    meeting_date = schema.Datetime(
        title='Date and Time',
        required=False,
    )
    directives.widget(
        'meeting_date',
        DatetimeFieldWidget,
        pattern_options={
            'time': {'interval': 60, 'min': [7, 0], 'max': [19, 0]}},
    )

13.6. Validation and default values#

In the following example we add a validator and a default value.

from zope.interface import Invalid
import datetime


def future_date(value):
    if value and not value.date() >= datetime.date.today():
        raise Invalid('Meeting date can not be before today.')
    return True


def meeting_date_default_value():
    return datetime.datetime.today() + datetime.timedelta(7)


class IMeeting(model.Schema):

    meeting_date = schema.Datetime(
        title='Date and Time',
        required=False,
        constraint=future_date,
        defaultFactory=meeting_date_default_value,
    )

Validators and defaults can also be made aware of the context (i.e. to check against the values of other fields).

For context aware defaults you need to use a IContextAwareDefaultFactory. It will be passed the container for which the add form is being displayed:

from zope.interface import provider
from zope.schema.interfaces import IContextAwareDefaultFactory


@provider(IContextAwareDefaultFactory)
def get_container_id(context):
    return context.id.upper()


class IMySchema(model.Schema):

    parent_id = schema.TextLine(
        title='Parent ID',
        required=False,
        defaultFactory=get_container_id,
    )

For context-aware validators you need to use invariant():

from zope.interface import Invalid
from zope.interface import invariant
from zope.schema.interfaces import IContextAwareDefaultFactory


class IMyEvent(model.Schema):

    start = schema.Datetime(
        title='Start date',
        required=False,
    )

    end = schema.Datetime(
        title='End date',
        required=False,
    )

    @invariant
    def validate_start_end(data):
        if data.start is not None and data.end is not None:
            if data.start > data.end:
                raise Invalid('Start must be before the end.')