April 25, 2023

How to Create a Custom Content Type With an Addon in Plone

Plone is an effective content management system (CMS) that enables users to create, edit, and publish content on the web. Content types are a key concept in Plone. A content type in Plone is essentially a blueprint or template for a specific type of content, like a news article, blog post, or product listing.

In Plone, a content type describes the workflow and permissions associated with the content type as well as the fields and metadata that make up the content. It also describes the structure and behavior of the content it represents. Plone makes sure that content is entered and managed consistently across the site by defining these parameters in a content type, making it simpler to organize and search.

Plone’s highly flexible and customizable content types enable developers to produce new content types that address particular requirements. For defining and managing content types, Plone offers a number of tools, such as:

Archetypes: Archetypes is the original content type framework for Plone. It provides a powerful, object-oriented way to define content types with complex fields, workflows, and behaviors. (Archetypes are depreciated in Plone 6)

Dexterity: Dexterity is a newer content type framework that provides a simpler, more flexible way to define content types using a schema-based approach. Dexterity content types can be defined entirely through code or through the Plone web interface.

Products.ContentTypes: This is a package that provides a set of pre-defined content types that can be used out of the box, including Page, News Item, and File.

plone.app.content types: This is another package that provides a set of pre-defined content types specifically designed for Plone 5 and later, including Event, Folder, and Image.

Users can create, edit, and publish content of a specific type using the Plone web interface once the content type has been defined and registered. According to the criteria specified in the content type, the content will be automatically organized and presented, making it simple to manage and locate.
To create custom content with an addon, follow the following steps :

First, create an addon using plonecli ->

Skip this part if you already installed plonecli

Step 1: Create a directory and create a python virtual environment in it by the following command –

sudo apt install python3.8-venv
mkdir plonecli
cd ploinecli
source bin/activate
pip install plonecli

Step 2: Create an addon using plonecli

Plonecli create addon example.addon

The type registration
Step 3: Register the content type by adding the below code snippet to “example.addon/src/example/addon/profiles/default/types.xml”

<?xml version="1.0"?>
<object name="portal_types" meta_type="Plone Types Tool">
 <object name="article" meta_type="Dexterity FTI"/>
</object>

Plone will now expect the presence of a file named article.xml in profiles/default/types and register it as a new content type.

Adding The FTI
Factory Type Information that holds the configuration for the content type article

Step 4: add the file “example.addon/src/example/addon/profiles/default/types/article.xml”.

<?xml version="1.0"?>
<object name="talk" meta_type="Dexterity FTI" i18n:domain="plone"
  xmlns:i18n="http://xml.zope.org/namespaces/i18n">
<property name="title" i18n:translate="">Article</property>
<property name="description" i18n:translate=""></property>
<property name="icon_expr">string:${portal_url}/document_icon.png</property>
<property name="factory">article</property>
<property name="add_view_expr">string:${folder_url}/++add++article</property>
<property name="link_target"></property>
<property name="immediate_view">view</property>
<property name="global_allow">True</property>
<property name="filter_content_types">True</property>
<property name="allowed_content_types"/>
<property name="allow_discussion">False</property>
<property name="default_view">view</property>
<property name="view_methods">
 <element value="view"/>
</property>
<property name="default_view_fallback">False</property>
<property name="add_permission">cmf.AddPortalContent</property>
<property name="klass">example.addon.content.article.Article</property>
<property name="schema">example.addon.content.article.IArticle</property>
<property name="behaviors">
 <element value="plone.dublincore"/>
 <element value="plone.namefromtitle"/>
 <element value="plone.versioning" />
</property>
<property name="model_source"></property>
<property name="model_file"></property>
<property name="schema_policy">dexterity</property>
<alias from="(Default)" to="(dynamic view)"/>
<alias from="edit" to="@@edit"/>
<alias from="sharing" to="@@sharing"/>
<alias from="view" to="(selected layout)"/>
<action title="View" action_id="view" category="object" condition_expr=""
   description="" icon_expr="" link_target="" url_expr="string:${object_url}"
   visible="True">
 <permission value="View"/>
</action>
<action title="Edit" action_id="edit" category="object" condition_expr=""
   description="" icon_expr="" link_target=""
   url_expr="string:${object_url}/edit" visible="True">
 <permission value="Modify portal content"/>
</action>
</object>

Our package’s Generic Setup configuration has now been updated. From the folder profiles/, Generic Setup loads a variety of configurations for the website. When you install the package, this configuration is applied to your website.

The schema
The fields that the content type will offer to store data are defined in the schema.

Step 5: add the schema to the addon
This is where you would add field-specific widget options to manage how fields are displayed.

The Python path “example.addon.content.article.IArticle” was referred to in the FTI.

There is no content for the module. Add an empty __init .py file to the folder content. from the example.addon/src/example/addon/content/ init .py.

Add a file called article.py with the following contents to this new folder:

from plone import schema
from plone.app.textfield import RichText
from plone.autoform import directives
from plone.dexterity.content import Container
from plone.namedfile.field import NamedBlobImage
from plone.schema.email import Email
from plone.supermodel import model
from z3c.form.browser.checkbox import CheckBoxFieldWidget
from z3c.form.browser.radio import RadioFieldWidget
from zope.interface import implementer
from zope.schema.vocabulary import SimpleTerm
from zope.schema.vocabulary import SimpleVocabulary


class IArticle(model.Schema):
   """Dexterity-Schema for Talks"""


   content = RichText(
       title='content',
       description='Description of the talk (max. 2000 characters)',
       max_length=2000,
       required=True,
   )
   speaker = schema.TextLine(
       title='Speaker',
       description='Name (or names) of the speaker',
       required=False,
   )

 


@implementer(IArticle)
class Article(Container):
   """Talk instance class"""

Now add your add-on to buildout.cfg

[buildout]
extends = https://dist.plone.org/release/6.0.0/versions.cfg
parts = instance

extensions =
        mr.developer
auto-checkout +=
    example.addon

[instance]
recipe = plone.recipe.zope2instance

eggs =
   Plone
   plone.volto
   Example.addon

user = admin:admin

[sources]
example.addon = fs example.addon path=src

Next, compile your plone instance and run it by:

./bin/buildout
./bin/instance fg

Step 6: Install the addon and try the addon to check if it’s available

After installing it, add the following content type in a folder/volto

Now add the content by selecting it.

Here we can see the fields defined in the schema, and from here, we can add the contents.

Here is our Content.
For different types of fields in your Schema, you can refer to the references below and add the fields according to your requirements.

from plone.app.multilingual.browser.interfaces import make_relation_root_path
from plone.app.textfield import RichText
from plone.app.z3cform.widget import AjaxSelectFieldWidget
from plone.app.z3cform.widget import RelatedItemsFieldWidget
from plone.app.z3cform.widget import SelectFieldWidget
from plone.autoform import directives
from plone.dexterity.content import Container
from plone.namedfile.field import NamedBlobFile
from plone.namedfile.field import NamedBlobImage
from plone.schema.email import Email
from plone.supermodel import model
from plone.supermodel.directives import fieldset
from plone.supermodel.directives import primary
from z3c.form.browser.checkbox import CheckBoxFieldWidget
from z3c.form.browser.radio import RadioFieldWidget
from z3c.relationfield.schema import Relation
from z3c.relationfield.schema import RelationChoice
from z3c.relationfield.schema import RelationList
from zope import schema
from zope.interface import implementer


class IExample(model.Schema):
   """Dexterity-Schema with all field-types."""

   # The most used fields
   # textline, text, bool, richtext, email

   fieldset(
       'numberfields',
       label='Number fields',
       fields=('int_field', 'float_field'),
   )

   fieldset(
       'datetimefields',
       label='Date and time fields',
       fields=('datetime_field', 'date_field', 'time_field', 'timedelta_field'),
   )

   fieldset(
       'choicefields',
       label='Choice and Multiple Choice fields',
       fields=(
           'choice_field',
           'choice_field_radio',
           'choice_field_select',
           'choice_field_voc',
           'list_field',
           'list_field_checkbox',
           'list_field_select',
           'list_field_voc_unconstrained',
           'tuple_field',
           'set_field',
           'set_field_checkbox',
       ),
   )

   fieldset(
       'relationfields',
       label='Relation fields',
       fields=('relationchoice_field', 'relationlist_field'),
   )

   fieldset(
       'filefields',
       label='File fields',
       fields=('file_field', 'image_field'),
   )

   fieldset(
       'otherfields',
       label='Other fields',
       fields=(
           'uri_field',
           'sourcetext_field',
           'ascii_field',
           'bytesline_field',
           'asciiline_field',
           'pythonidentifier_field',
           'dottedname_field',
           'dict_field',
           'dict_field_with_choice',
       ),
   )

   primary('title')
   title = schema.TextLine(
       title='Primary Field (Textline)',
       required=True,
   )

   text_field = schema.Text(
       title='Text Field',
       required=False,
       missing_value='',
   )

   textline_field = schema.TextLine(
       title='Textline field',
       description='A simple input field',
       required=False,
   )

   bool_field = schema.Bool(
       title='Boolean field',
       required=False,
   )

   choice_field = schema.Choice(
       title='Choice field',
       values=['One', 'Two', 'Three'],
       required=True,
   )

   directives.widget(choice_field_radio=RadioFieldWidget)
   choice_field_radio = schema.Choice(
       title='Choice field with radio boxes',
       values=['One', 'Two', 'Three'],
       required=True,
   )

   choice_field_voc = schema.Choice(
       title='Choicefield with values from named vocabulary',
       vocabulary='plone.app.vocabularies.PortalTypes',
       required=False,
   )

   directives.widget(choice_field_select=SelectFieldWidget)
   choice_field_select = schema.Choice(
       title='Choicefield with select2 widget',
       vocabulary='plone.app.vocabularies.PortalTypes',
       required=False,
   )

   list_field = schema.List(
       title='List field',
       value_type=schema.Choice(
           values=['Beginner', 'Advanced', 'Professional'],
       ),
       required=False,
       missing_value=[],
   )

   directives.widget(list_field_checkbox=CheckBoxFieldWidget)
   list_field_checkbox = schema.List(
       title='List field with checkboxes',
       value_type=schema.Choice(
           values=['Beginner', 'Advanced', 'Professional'],
       ),
       required=False,
       missing_value=[],
   )

   directives.widget(list_field_select=SelectFieldWidget)
   list_field_select = schema.List(
       title='List field with select widget',
       value_type=schema.Choice(
           values=['Beginner', 'Advanced', 'Professional'],
       ),
       required=False,
       missing_value=[],
   )

   list_field_voc_unconstrained = schema.List(
       title='List field with values from vocabulary but not constrained to them.',
       value_type=schema.TextLine(),
       required=False,
       missing_value=[],
   )
   directives.widget(
       'list_field_voc_unconstrained',
       AjaxSelectFieldWidget,
       vocabulary='plone.app.vocabularies.Users',
   )

   tuple_field = schema.Tuple(
       title='Tuple field',
       value_type=schema.Choice(
           values=['Beginner', 'Advanced', 'Professional'],
       ),
       required=False,
       missing_value=(),
   )

   set_field = schema.Set(
       title='Set field',
       value_type=schema.Choice(
           values=['Beginner', 'Advanced', 'Professional'],
       ),
       required=False,
       missing_value=set(),
   )

   directives.widget(set_field_checkbox=CheckBoxFieldWidget)
   set_field_checkbox = schema.Set(
       title='Set field with checkboxes',
       value_type=schema.Choice(
           values=['Beginner', 'Advanced', 'Professional'],
       ),
       required=False,
       missing_value=set(),
   )

   # File fields
   image_field = NamedBlobImage(
       title='Image field',
       description='A upload field for images',
       required=False,
   )

   file_field = NamedBlobFile(
       title='File field',
       description='A upload field for files',
       required=False,
   )

   # Date and Time fields
   datetime_field = schema.Datetime(
       title='Datetime field',
       description='Uses a date and time picker',
       required=False,
   )

   date_field = schema.Date(
       title='Date field',
       description='Uses a date picker',
       required=False,
   )

   time_field = schema.Time(
       title='Time field',
       required=False,
   )

   timedelta_field = schema.Timedelta(
       title='Timedelta field',
       required=False,
   )

   # Relation Fields
   relationchoice_field = RelationChoice(
       title='Relationchoice field',
       vocabulary='plone.app.vocabularies.Catalog',
       required=False,
   )
   directives.widget(
       'relationchoice_field',
       RelatedItemsFieldWidget,
       pattern_options={
           'selectableTypes': ['Document'],
           'basePath': make_relation_root_path,
       },
   )

   relationlist_field = RelationList(
       title='Relationlist Field',
       default=[],
       value_type=RelationChoice(vocabulary='plone.app.vocabularies.Catalog'),
       required=False,
       missing_value=[],
   )
   directives.widget(
       'relationlist_field',
       RelatedItemsFieldWidget,
       pattern_options={
           'selectableTypes': ['Document'],
           'basePath': make_relation_root_path,
       },
   )

   # Number fields
   int_field = schema.Int(
       title='Integer Field (e.g. 12)',
       description='Allocated (maximum) number of objects',
       required=False,
   )

   float_field = schema.Float(
       title='Float field (e.g. 12.2)',
       required=False,
   )

   # Text fields
   email_field = Email(
       title='Email field',
       description='A simple input field for a email',
       required=False,
   )

   uri_field = schema.URI(
       title='URI field',
       description='A simple input field for a URLs',
       required=False,
   )

   richtext_field = RichText(
       title='RichText field',
       description='This uses a richtext editor.',
       max_length=2000,
       required=False,
   )

   sourcetext_field = schema.SourceText(
       title='SourceText field',
       required=False,
   )

   ascii_field = schema.ASCII(
       title='ASCII field',
       required=False,
   )

   bytesline_field = schema.BytesLine(
       title='BytesLine field',
       required=False,
   )

   asciiline_field = schema.ASCIILine(
       title='ASCIILine field',
       required=False,
   )

   pythonidentifier_field = schema.PythonIdentifier(
       title='PythonIdentifier field',
       required=False,
   )

   dottedname_field = schema.DottedName(
       title='DottedName field',
       required=False,
   )

   dict_field = schema.Dict(
       title='Dict field',
       required=False,
       key_type=schema.TextLine(
           title='Key',
           required=False,
       ),
       value_type=schema.TextLine(
           title='Value',
           required=False,
       ),
   )

   dict_field_with_choice = schema.Dict(
       title='Dict field with key and value as choice',
       required=False,
       key_type=schema.Choice(
           title='Key',
           values=['One', 'Two', 'Three'],
           required=False,
       ),
       value_type=schema.Set(
           title='Value',
           value_type=schema.Choice(
               values=['Beginner', 'Advanced', 'Professional'],
           ),
           required=False,
           missing_value={},
       ),
   )


@implementer(IExample)
class Example(Container):
   """Example instance class"""