Skip to content

Commit 6b634d7

Browse files
authored
Merge pull request plone#1372 from plone/behaviors
Behavior chapter from scratch
2 parents d3e2a00 + 8853844 commit 6b634d7

File tree

2 files changed

+372
-4
lines changed

2 files changed

+372
-4
lines changed

docs/backend/behaviors.md

Lines changed: 355 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,363 @@ myst:
1111

1212
# Behaviors
1313

14+
In Plone, behaviors are a way to add reusable functionality to content objects without modifying the objects themselves.
15+
Behaviors are essentially small chunks of code that can be plugged onto content types to provide new features or capabilities.
16+
17+
A Plone behavior could be used to
18+
19+
- add a set of form fields (on standard add and edit forms),
20+
- add logic as part of the adapter,
21+
- enable a particular event handler,
22+
- enable one or more views, viewlets, or other UI components,
23+
- do anything else which may be expressed in code via an adapter or marker interface.
24+
25+
Behaviors can be added to content types on an as-needed basis, allowing for a high degree of flexibility and customization.
26+
27+
Plone already provides lots of behaviors for the built-in content types.
28+
29+
Other behaviors are implemented as add-on products, which can be installed and configured through the Plone control panel.
30+
Once a behavior has been installed, it can be applied to any content type by selecting it in the {guilabel}`Content Types` control panel.
31+
This allows items of this content type to gain the additional functionality provided by the behavior.
32+
33+
A key feature of behaviors is that they allow encapsulating functionality so that it can be reused for multiple content types without needing to reimplement it.
34+
Overall, behaviors are an important part of the Plone content management system and allow for powerful customization and extensibility of content objects.
35+
36+
37+
## Built-in behaviors
38+
39+
To view a complete list of built-in behaviors, browse to {guilabel}`Content Types` control panel, then click {guilabel}`Page` (or any other content type), then {guilabel}`Behaviors`.
40+
41+
| short name | Title | Desription |
42+
|---|---|---|
43+
| `plone.allowdiscussion` | Allow discussion | Allow discussion on this item |
44+
| `plone.basic` | Basic metadata | Adds title and description fields. |
45+
| `volto.blocks` | Blocks | Enables Volto Blocks support |
46+
| `volto.blocks.editable.layout` | Blocks (Editable Layout) | Enables Volto Blocks (editable layout) support |
47+
| `plone.categorization` | Categorization | Adds keywords and language fields. |
48+
| `plone.collection` | Collection | Adds collection behavior |
49+
| `plone.publication` | Date range | Adds effective date and expiration date fields. |
50+
| `plone.dublincore` | Dublin Core metadata | Adds standard metadata fields (equals Basic metadata + Categorization + Effective range + Ownership |
51+
| `plone.eventattendees` | Event Attendees | Attendees extension for Events. |
52+
| `plone.eventbasic` | Event Basic | Basic Event schema. |
53+
| `plone.eventcontact` | Event Contact | Contact extension for Events. |
54+
| `plone.eventlocation` | Event Location | Location extension for Events. |
55+
| `plone.eventrecurrence` | Event Recurrence | Recurrence extension for Events. |
56+
| `plone.excludefromnavigation` | Exclude From navigation | Allow items to be excluded from navigation |
57+
| `plone.constraintypes` | Folder Addable Constrains | Restrict the content types that can be added to folderish content |
58+
| `plone.textindexer` | Full-Text Indexing | Enables the enhanced full-text indexing for a content type. If a field in the schema is marked with the `searchable` directive, its content gets added to the `SearchableText` index in the catalog |
59+
| `volto.head_title` | Head title field | Adds Head title field |
60+
| `plone.leadimage` | Lead Image | Adds image and image caption fields |
61+
| `plone.locking` | Locking | Locking support for dexterity |
62+
| `plone.translatable` | Multilingual Support | Make this content type multilingual aware. Multilingual support must be installed. |
63+
| `plone.namefromfilename` | Name from file name | Automatically generate short URL name for content based on its primary field file name
64+
| `plone.namefromtitle` | Name from title | Automatically generate short URL name for content based on its initial title
65+
| `plone.navigationroot` | Navigation root | Make all items of this type a navigation root |
66+
| `volto.navtitle` | Navigation title | Navigation title used in sections, menus and doormats |
67+
| `plone.nextpreviousenabled` | Next previous navigation | Enable next previous navigation for all items of this type |
68+
| `plone.nextprevioustoggle` | Next previous navigation toggle | Allow items to have next previous navigation enabled |
69+
| `plone.ownership` | Ownership | Adds creator, contributor, and rights fields. |
70+
| `volto.preview_image` | Preview Image | Preview image for listings |
71+
| `volto.preview_image_link` | Preview Image Link | Preview image for listings based on links |
72+
| `plone.relateditems` | Related items | Adds the ability to assign related items |
73+
| `plone.richtext` | RichText | Adds richtext behavior |
74+
| `plone.shortname` | Short name | Gives the ability to rename an item from its edit form. |
75+
| `plone.tableofcontents` | Table of contents | Adds a table of contents |
76+
| `plone.thumb_icon` | Thumbs and icon handling | Options to suppress thumbs and/or icons and to override thumb size in listings, tables etc.
77+
| `plone.versioning` | Versioning | Versioning support with CMFEditions |
78+
79+
```{todo}
80+
For each behavior in the table above, one may view the source code of the checkbox (its `name` attribute) to view its Short Name.
81+
An issue has been created to better expose these in the user interface.
82+
[Control panel for Content Type > Behaviors short names not displayed to user Products.CMFPlone#3706](https://github.com/plone/Products.CMFPlone/issues/3706)
83+
```
84+
85+
## Adding or removing a behavior from a content type
86+
87+
There are two ways to add or remove a behavior on a content type:
88+
89+
- Through the web using the {guilabel}`Content Types` control panel.
90+
- Using a custom add-on `GenericSetup` profile.
91+
92+
93+
### Through the web
94+
95+
1. Go to the {guilabel}`Site Setup` and chose the {guilabel}`Content Types` control panel.
96+
2. Select the content type to which you want to add or remove a behavior.
97+
3. Then click on the {guilabel}`Behaviors` tab of the settings of the content type.
98+
4. A list of all available behaviors appears.
99+
Select or deselect the checkbox of the behavior you want to add to or remove from the type.
100+
5. Save the form by clicking on the {guilabel}`Save` button at the bottom of the page.
101+
102+
103+
### Using a `GenericSetup` profile
104+
105+
Given you already have a custom add-on with a `profiles/default` directory, and you created a custom behavior named `mybehavior.subtitle`.
106+
107+
If you want to enable a behavior on an existing content type, create a new directory `types` under `profiles/default`.
108+
In the `types` directory, create a file named the same as the content type you want to change.
109+
In the example here, you want to add a behavior to the built-in `Event` content type.
110+
Create a file named `Event.xml`.
111+
It is a {term}`Factory Type Information` (FTI) definition.
112+
You need to change only the behavior's configuration.
113+
All other parts can be ignored.
114+
The file `Event.xml` contains the following.
115+
116+
```xml
117+
<?xml version="1.0"?>
118+
<object
119+
i18n:domain="plone"
120+
meta_type="Dexterity FTI"
121+
name="Event"
122+
xmlns:i18n="http://xml.zope.org/namespaces/i18n">
123+
<property name="behaviors" purge="false">
124+
<element value="myproject.subtitle" />
125+
</property>
126+
</object>
127+
```
128+
129+
After you apply the profile (or uninstall and install the custom add-on), the behavior is effective on the `Event` content type.
130+
131+
132+
## Custom behaviors
133+
134+
There are two types of behaviors:
135+
136+
Schema-only behaviors
137+
: These behaviors have only a schema with fields.
138+
139+
Full behaviors
140+
: A Python class containing the logic of the behavior, an interface or schema defining the contract of the behavior, and a marker interface applicable to a content type.
141+
142+
143+
### Create a schema-only behavior
144+
145+
Given you want to add a field `subtitle` to some existing content types of your custom add-on.
146+
147+
You need to create a file `subtitle.py` in your add-on:
148+
149+
```python
150+
from plone.autoform.interfaces import IFormFieldProvider
151+
from plone.supermodel import model
152+
from zope import schema
153+
from zope.interface import provider
154+
155+
156+
@provider(IFormFieldProvider)
157+
class ISubtitleBehavior(model.Schema):
158+
"""Subtitle behavior."""
159+
160+
subtitle = schema.Text(
161+
title="Subtitle",
162+
description="A title to be displayed below the title",
163+
default="",
164+
required=False,
165+
)
166+
```
167+
168+
You need to add a ZCML snippet to the `configure.zcml` next to `subtitle.py`:
169+
170+
```xml
171+
<plone:behavior
172+
name="myproject.subtitle"
173+
provides=".subtitle.ISubtitleBehavior"
174+
title="Subtitle"
175+
/>
176+
```
177+
178+
After a restart of Plone, the behavior can be added to the content type in the {guilabel}`Content Types` control panel.
179+
The add and edit forms contain a new field `Subtitle`.
180+
181+
This field is not displayed in most views.
182+
To display the entered data in this field, you need to modify the page template by adding the field `context.subtitle`.
183+
184+
### Creating a behavior with an adapter and factory
185+
186+
Given you want to display a price with different content types.
187+
The price is stored as the net value on the type as a floating point number.
188+
For display, you need at several places the value added tax (VAT) and the gross value.
189+
190+
You create a schema with the net value for the form and attributes for the calculated values.
191+
You create an adapter to calculate the VAT and gross values.
192+
You need a marker interface to distinguish between context and adapter.
193+
194+
Add Python code in the file `price.py`:
195+
196+
```python
197+
from plone.autoform.interfaces import IFormFieldProvider
198+
from plone.supermodel import model
199+
from zope import schema
200+
from zope.interface import Attribute
201+
from zope.interface import implementer
202+
from zope.interface import Interface
203+
from zope.interface import provider
204+
205+
206+
@provider(IFormFieldProvider)
207+
class IPriceBehavior(model.Schema):
208+
"""Behavior: a price, VAT and gross."""
209+
210+
price_net = schema.Float(
211+
title="Price (net)",
212+
required=True,
213+
)
214+
price_vat = Attribute("VAT 20% of net price")
215+
price_gross = Attribute("Price gross (net + VAT 20%")
216+
217+
218+
class IPriceMarker(Interface):
219+
"""Marker for content that has a price."""
220+
221+
222+
@implementer(IPriceBehavior)
223+
class PriceAdapter:
224+
def __init__(self, context):
225+
self.context = context
226+
227+
@property
228+
def price_net(self):
229+
"""Getter, read from context and return back"""
230+
return self.context.price_net
231+
232+
@price_net.setter
233+
def price_net(self, value):
234+
"""Setter, called by the form, set on context"""
235+
self.context.price_net = value
236+
237+
@property
238+
def price_vat(self):
239+
return self.price_net * 0.2
240+
241+
@property
242+
def price_gross(self):
243+
return self.price_net + self.price_vat
244+
```
245+
246+
The registration in the `configure.zcml`:
247+
248+
```xml
249+
<plone:behavior
250+
factory=".price.PriceAdapter"
251+
for=".price.IPriceMarker"
252+
marker=".price.IPriceMarker"
253+
name="myproject.price"
254+
provides=".price.IPriceBehavior"
255+
title="Price with net, VAT and gross"
256+
/>
257+
```
258+
259+
After a restart of Plone, the behavior can be added to the content type in the {guilabel}`Content Types` control panel.
260+
The add and edit forms contain a new field `Price (net)`.
261+
262+
This field is not displayed in most views.
263+
To display the entered data in this field, you need to modify the page template by adding the `price_net` field as `context.price_net`.
264+
To access the `price_vat` and `price_gross` fields from a browser view, you need to get the adapter from the context of the view:
265+
266+
(behavior-code-example)=
267+
268+
```python
269+
from .price import IPriceBehavior
270+
271+
class SomeViewClass:
272+
273+
def vat(self):
274+
price_for_context = IPriceBehavior(context)
275+
return price_for_context.price_vat
276+
277+
def gross(self):
278+
price_for_context = IPriceBehavior(context)
279+
return price_for_context.price_gross
280+
```
281+
282+
### Create a behavior with PloneCLI
283+
284+
To add a behavior to your add-on, you can use PloneCLI as follows:
285+
286+
```shell
287+
plonecli add behavior
288+
```
289+
290+
This will create the behavior Python file in the `behaviors` folder where you can define your behavior's schema fields, and registers the behavior in the `configure.zcml`.
291+
292+
293+
### Further reading on working with behaviors
14294

15295
```{seealso}
16296
See the chapter {ref}`training:behaviors1-label` from the Mastering Plone 6 Training.
17297
```
18298

19-
```{todo}
20-
Contribute to this documentation!
21-
See issue [Backend > Behaviors needs content](https://github.com/plone/documentation/issues/1302).
22-
```
299+
300+
## How behaviors work
301+
302+
```{note}
303+
Skip this section if you do not want to dive deeper into the internals of behaviors.
304+
You do not *need* to know this, but it may help if you run into problems.
305+
```
306+
307+
In Plone, behaviors can be globally enabled on content types at runtime.
308+
With add-ons, behaviors can be enabled even on a single content object or for a whole subtree in the content hierarchy.
309+
310+
311+
### Interfaces and adapters
312+
313+
To explain interfaces and adapters, let's begin with an analogy using electrical systems.
314+
315+
An electrical outlet provides an interface through which electricity passes.
316+
When you travel to another country, you may need an outlet adapter for the outlet (the interface).
317+
For example, assume you have a device that has a plug for Schuko outlets, and in Italy there are Type L outlets.
318+
If we were to represent the behavior of choosing the correct outlet adapter in Plone, you would do the following.
319+
320+
- You need an outlet adapter for your Schuko plug.
321+
1. You look at the outlet and see it is Type L.
322+
2. You look in your box containing different adapters and choose the correct outlet adapter to use.
323+
3. You plug that into the wall outlet.
324+
4. Finally, you can use your Schuko providing device on an Italian Type L outlet.
325+
- In Python, you would call `getAdapter(context, ISchuko)` (context is here the outlet type), which would then do the following.
326+
1. Determine the type of interface provided by the `context`.
327+
As a result, it finds `ITypeL` interface.
328+
2. Looks in the component registry if there is a class that adapts to `ITypeL`.
329+
At the same time, it provides the requested `ISchuko` adapter.
330+
3. Initializes the adapter class with the context, and returns it as the result.
331+
4. Finally, the `ISchuko` providing adapter can be used on a `ITypeL` providing context.
332+
333+
This process of choosing the right adapter based on the information of the context and the requested interface implements the design pattern of an abstract factory.
334+
335+
Similarly, using the {ref}`behavior code example <behavior-code-example>` above:
336+
337+
- You would call an abstract factory with `getAdapter(context, IPriceBehavior)` to get an adapter, `price_for_context`.
338+
Although it is an interface, it is more of a shortcut to factory usage.
339+
- The adapter that is specific to the given content type is assigned to the variable `price_for_context`.
340+
Now you can use `price_for_context` for whatever you like.
341+
342+
When a behavior is enabled for a particular object, it will be possible to adapt that object to the behavior's interface.
343+
Otherwise, when the behavior is disabled, adaptation will fail or falls back to a more generic adapter, if any is registered.
344+
345+
A behavior is at least a combination of an interface (also as a form field provider); metadata such as a name, title, and description; and sometimes an adapter factory with a marker interface.
346+
When a behavior is enabled, an interface is added to the content object to indicate its presence.
347+
In other words, the content object now provides the interface.
348+
349+
Behaviors without an adapter factory can be used either as a simple marker or to provide additional form fields.
350+
In this case, adapting a content object with this interface returns the content object itself, because adapting an object that already provides the exact same interface returns the very same object.
351+
Based on the now-provided interface, specific views can be registered with the content type, or event handlers can be registered to respond to specific actions.
352+
353+
In other cases, there is also an adapter factory (usually a Python class), which will be invoked (initialized) to get an appropriate adapter when requested.
354+
If an adapter factory is used, an explicit marker interface is required.
355+
356+
With an adapter factory in place, custom getters and setters for form fields can be implemented, or even new methods.
357+
For example, calculations or the combination of data can be added.
358+
359+
### Registration
360+
361+
Behaviors are registered globally using the `<plone.behavior />` {term}`ZCML` directive.
362+
Internally, this directive registers a named utility that provides `plone.behavior.interfaces.IBehavior`.
363+
This utility contains combined information about the behavior, such as its name, interface, factory or marker interface, and metadata.
364+
365+
```{seealso}
366+
The [README file of `plone.behavior`](https://github.com/plone/plone.behavior/blob/master/README.rst) explains the concepts and different ways to register a behavior in detail.
367+
```
368+
369+
### Lookup and provide
370+
371+
Plone content objects have logic to look up the behaviors' names registered from their types' configuration, the {term}`Factory Type Information` (FTI).
372+
At runtime, the logic provides the interface (or marker) from the behavior to the object.
373+
This dynamically provided interface enables the component architecture to react to this new interface by adding additional form fields, bindings events, enabling more specific views, and more.

docs/glossary.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,21 @@ content rule
505505
trigger
506506
A trigger is an event in Plone that causes the execution of defined actions.
507507
Example triggers include object modified, user logged in, and workflow state changed.
508+
509+
FTI
510+
Factory Type Information
511+
Factory type information (FTI) is responsible for content creation in the portal.
512+
FTI is responsible for the following:
513+
514+
- Which function is called when new content type is added.
515+
- Icons available for content types.
516+
- Creation views for content types.
517+
- Permission and security.
518+
- Whether discussion is enabled.
519+
- Providing the `factory_type_information` dictionary.
520+
This is used elsewhere in the code (often in `__init__.py` of a product) to set the initial values for a ZODB Factory Type Information object (an object in the `portal_types` tool).
521+
522+
```{seealso}
523+
[`FactoryTypeInformation` class source code](https://github.com/zopefoundation/Products.CMFCore/blob/361a30e0c72a15a21f88433b8d5fc49331f36728/src/Products/CMFCore/TypesTool.py#L431)
524+
```
508525
```

0 commit comments

Comments
 (0)