13. Content types reference – Mastering Plone Development

13. Content types reference#

This chapter documents common fields, widgets, directives that you can use with content types.

Note

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