diff --git a/docs/backend/behaviors.md b/docs/backend/behaviors.md index 34dc351d4..9295a1452 100644 --- a/docs/backend/behaviors.md +++ b/docs/backend/behaviors.md @@ -34,6 +34,8 @@ A key feature of behaviors is that they allow encapsulating functionality so tha Overall, behaviors are an important part of the Plone content management system and allow for powerful customization and extensibility of content objects. +(backend-built-in-behaviors-label)= + ## Built-in behaviors 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`. diff --git a/docs/backend/content-types.md b/docs/backend/content-types/index.md similarity index 100% rename from docs/backend/content-types.md rename to docs/backend/content-types/index.md diff --git a/docs/backend/index.md b/docs/backend/index.md index 7326de984..405f55792 100644 --- a/docs/backend/index.md +++ b/docs/backend/index.md @@ -19,7 +19,7 @@ Insert introduction here. :maxdepth: 2 configuration-registry control-panels -content-types +content-types/index behaviors annotations vocabularies diff --git a/docs/contributing/documentation/admins.md b/docs/contributing/documentation/admins.md index 98d3c782e..03a459bb5 100644 --- a/docs/contributing/documentation/admins.md +++ b/docs/contributing/documentation/admins.md @@ -15,6 +15,43 @@ This guide is for administrators of Plone Documentation. It covers automated deployments, hosting, automated testing, previewing, and importing external package documentation into Plone Documentation. +(administrators-import-docs-submodule-label)= + +## Importing external docs and converting to MyST + +This section describes how to import external projects and convert their docs to MyST. +We did this for `plone.app.dexterity` and several other projects. + +1. Create a new branch using the name of the project. +1. Install [rst-to-myst](https://pypi.org/project/rst-to-myst/). + + ```shell + bin/pip install "rst-to-myst[sphinx]" + ``` + +1. Clone the project repository to the root of `plone/documentation`. +1. Delete any non-documentation files from the clone. +1. Move the documentation files and subfolders to the root of the clone, retaining the documentation structure. +1. Convert the reStructuredText documentation files to MyST. + The example commands below assume that there are files at the root of the clone and in one sub-level of nested directories. + For deeper nesting, insert globbing syntax for each sub-level as `**/` + + ```shell + bin/rst2myst convert -R project/*.rst + bin/rst2myst convert -R project/**/*.rst + ``` + +1. Add HTML meta data to the converted files. + + ```shell + cd project + ../bin/python ../docs/addMetaData.py + ``` + +1. Optionally clean up any MyST syntax. +1. Commit and push your branch to GitHub and create a pull request. + + (administrators-import-docs-submodule-label)= ## Importing external docs with submodules diff --git a/plone.app.dexterity/advanced/behaviors.md b/plone.app.dexterity/advanced/behaviors.md new file mode 100644 index 000000000..7961db893 --- /dev/null +++ b/plone.app.dexterity/advanced/behaviors.md @@ -0,0 +1,39 @@ +--- +myst: + html_meta: + "description": "How to find and add behaviors in Plone content types" + "property=og:description": "How to find and add behaviors in Plone content types" + "property=og:title": "How to find and add behaviors in Plone content types" + "keywords": "Plone, content types, behaviors" +--- + +# Behaviors + +This chapter describes how to find and add behaviors. + +Dexterity introduces the concept of *behaviors*, which are reusable bundles of functionality or form fields which can be turned on or off on a per-type basis. + +Each behavior has a unique interface. +When a behavior is enabled on a type, you will be able to adapt that type to the behavior's interface. +If the behavior is disabled, the adaptation will fail. +The behavior interface can also be marked as an `IFormFieldsProvider`, in which case it will add fields to the standard add and edit forms. +Finally, a behavior may imply a sub-type: a marker interface which will be dynamically provided by instances of the type for which the behavior is enabled. + +We will not cover writing new behaviors here, but we will show how to enable behaviors on a type. +Writing behaviors is covered in {doc}`/backend/behaviors`. + +In fact, we've already seen one standard behavior applied to our example types, registered in the FTI and imported using GenericSetup. + +```xml + + + +``` + +Other behaviors are added in the same way, by listing additional behavior interfaces as elements of the `behaviors` property. + +Behaviors are normally registered with the `` ZCML directive. +When registered, a behavior will create a global utility providing `IBehavior`, which is used to provide some metadata, such as a title and description for the behavior. + +You can find and apply behaviors via the {guilabel}`Dexterity Content Types` control panel that is installed with [`plone.app.dexterity`](https://pypi.org/project/plone.app.dexterity/). +For a list of standard behaviors that ship with Plone, see {ref}`backend-built-in-behaviors-label`. \ No newline at end of file diff --git a/plone.app.dexterity/advanced/catalog-indexing-strategies.md b/plone.app.dexterity/advanced/catalog-indexing-strategies.md new file mode 100644 index 000000000..46be0ae53 --- /dev/null +++ b/plone.app.dexterity/advanced/catalog-indexing-strategies.md @@ -0,0 +1,306 @@ +--- +myst: + html_meta: + "description": "Catalog indexing strategies of content types in Plone" + "property=og:description": "Catalog indexing strategies of content types in Plone" + "property=og:title": "Catalog indexing strategies of content types in Plone" + "keywords": "Plone, content types, catalog, indexing, strategies" +--- + +# Catalog indexing strategies + +This chapter describes catalog indexing strategies. + +You may have two different interests in regard to indexing your custom content type objects: + +- Making particular fields searchable via Plone's main search facility. +- Indexing particular fields for custom lookup. + + +## Making content searchable + +Plone's main index is called *SearchableText*. +This is the index which is searched when you use the main portal search. +Fields in your custom content types are not necessarily added to SearchableText. +Fields added via Dublin-core behaviors are automatically part of SearchableText; others are not. + +You may need to explicitly add fields to SearchableText if you wish their information to be findable via the main search. +There are all sorts of highly customizable ways to do this, but the easiest is to use the the behavior `plone.textindexer` that is shipped with `plone.app.dexterity`. + +It allows you to easily add fields to SearchableText. +Once you turn on this behavior, you will then need to specify fields for addition to SearchableText. + +```{note} +If you turn on the `Full-Text Indexing` behavior for a content type, then you must specify all fields that need SearchableText indexing. +Dublin core fields, including Title and Description, are no longer automatically handled. +``` + +Once you have turned on the indexer behavior, edit the XML field model to add `indexer:searchable="true"` to the `field` tag for each field you wish to add to the SearchableText index. + +See {doc}`/backend/indexing` documentation for details and for information on how to use it via Python schema. + + +## Creating and using custom indexes + +This section describes how to create custom catalog indexes. + +The ZODB is a hierarchical object store where objects of different schemata and sizes can live side by side. +This is great for managing individual content items, but not optimal for searching across the content repository. +A naive search would need to walk the entire object graph, loading each object into memory and comparing object metadata with search criteria. +On a large site, this would quickly become prohibitive. + +Luckily, Zope comes with a technology called the *ZCatalog*, which is basically a table structure optimized for searching. +In Plone, there's a ZCatalog instance called `portal_catalog`. +Standard event handlers will index content in the catalog when it is created or modified, and unindex when the content is removed. + +The catalog manages *indexes*, which can be searched, and *metadata* (also known as *columns*), which are object attributes for which the value is copied into the catalog. +When we perform a search, the result is a lazily loaded list of objects known as *catalog brains*. +Catalog brains contain the value of metadata columns (but not indexes) as attributes. +The functions `getURL()`, `getPath()`, and `getObject()` can be used to get the URL and path of the indexed content item, and to load the full item into memory. + +```{note} +If you're working with references, parent objects, or a small number of child objects, it is usually OK to load objects directly to work with them. +However, if you are working with a large or unknown-but-potentially-large number of objects, you should consider using catalog searches to find them, and use catalog metadata to store frequently used values. +There is an important trade-off to be made between limiting object access and bloating the catalog with unneeded indexes and metadata, though. +In particular, large strings (such as the body text of a document) or binary data (such as the contents of image or file fields) should not be stored as catalog metadata. +``` + +Plone comes with a number of standard indexes and metadata columns. +These correspond to much of the *Dublin Core* set of metadata as well as several Plone-specific attributes. +You can view the indexes, columns, and the contents of the catalog through the ZMI pages of the `portal_catalog` tool. +If you've never done this, it is probably instructive to have a look, both to understand how the indexes and columns may apply to your own content types, and to learn what searches are already possible. + +Indexes come in various types. The most common ones are the following. + +`FieldIndex` +: The most common type, used to index a single value. + +`KeywordIndex` +: Used to index lists of values where you want to be able to search for a subset of the values. + As the name implies, commonly used for keyword fields, such as the `Subject` Dublin Core metadata field. + +`DateIndex` +: Used to index Zope 2 `DateTime` objects. + Note that if your type uses a *Python* `datetime` object, you'll need to convert it to a Zope 2 `DateTime` using a custom indexer! + +`DateRangeIndex` +: Used mainly for the effective date range. + +`ZCTextIndex` +: Used mainly for the `SearchableText` index. + This is the index used for full text search. + +`ExtendedPathIndex` +: A variant of `PathIndex`, which is used for the `path` index. + This is used to search for content by path and optionally depth. + + +### Adding new indexes and metadata columns + +When an object is indexed, by default the catalog will attempt to find attributes and methods that match index and column names on the object. +Methods will be called with no arguments in an attempt to get a value. +If a value is found, it is indexed. + +```{note} +Objects are normally acquisition-wrapped when they are indexed, which means that an indexed value may be acquired from a parent. +This can be confusing, especially if you are building container types and creating new indexes for them. +If child objects don't have attributes or methods with names corresponding to indexes, the parent object's value will be indexed for all children as well. +``` + +Catalog indexes and metadata can be installed with the `catalog.xml` GenericSetup import step. +It is useful to look at the one in Plone, located at {file}`parts/omelette/Products/CMFPlone/profiles/default/catalog.xml`. + +As an example, let's index the `track` property of a `Session` in the catalog, and add a metadata column for this property as well. +In {file}`profiles/default/catalog.xml`, we have the following code. + +```xml + + + + + + + +``` + +Notice how we specify both the index name and the indexed attribute. +It is possible to use an index name (the key you use when searching) that is different to the indexed attribute, although they are usually the same. +The metadata column is just the name of an attribute. + + +### Creating custom indexers + +Indexing based on attributes sometimes can be limiting. +First of all, the catalog is indiscriminate in that it attempts to index every attribute that's listed against an index or metadata column for every object. +Secondly, it is not always feasible to add a method or attribute to a class just to calculate an indexed value. + +Plone 3.3 and later ships with a package called [`plone.indexer`](https://pypi.org/project/plone.indexer/) to help make it easier to write custom indexers. +These are components that are invoked to calculate the value which the catalog sees when it tries to index a given attribute. +Indexers can be used to index a different value to the one stored on the object, or to allow indexing of a "virtual" attribute that does not actually exist on the object in question. +Indexers are usually registered on a per-type basis, so you can have different implementations for different types of content. + +To illustrate indexers, we will add three indexers to {file}`program.py`. +Two will provide values for the `start` and `end` indexes, normally used by Plone's `Event` type. +We actually have attributes with the correct name for these already, but they use Python `datetime` objects whereas the `DateIndex` requires a Zope 2 `DateTime.DateTime` object. +Python didn't have a `datetime` module when this part of Zope was created! +The third indexer will be used to provide a value for the `Subject` index that takes its value from the `tracks` list. + +```python +from DateTime import DateTime +from plone.indexer import indexer +... + +@indexer(IProgram) +def startIndexer(obj): + if obj.start is None: + return None + return DateTime(obj.start.isoformat()) + +@indexer(IProgram) +def endIndexer(obj): + if obj.end is None: + return None + return DateTime(obj.end.isoformat()) + +@indexer(IProgram) +def tracksIndexer(obj): + return obj.tracks +``` + +And we need to register the indexers in ZCML. + +```xml + + + +``` + +Here, we use the `@indexer` decorator to create an indexer. +This doesn't register the indexer component, though, so we need to use ZCML to finalize the registration. +Crucially, this is where the indexer's `name` is defined. +This is the name of the indexed attribute for which the indexer is providing a value. + +```{note} +Since all of these indexes are part of a standard Plone installation, we won't register them in `catalog.xml`. +If you create custom indexers and need to add new catalog indexes or columns for them, remember that the "indexed attribute" name and the column name must match the name of the indexer as set in its adapter registration. +``` + + +### Searching using your indexes + +Once we have registered our indexers and re-installed our product to ensure that the `catalog.xml` import step is allowed to install new indexes in the catalog, we can use our new indexes just like we would any of the default indexes. + +The pattern is always the same, as shown below. + +```python +from plone import api +# get the tool +catalog = api.portal.get_tool(name='portal_catalog') +# execute a search +results = catalog(track='Track 1') +# examine the results +for brain in results: + start = brain.start + url = brain.getURL() + obj = brain.getObject() # Performance hit! +``` + +This shows a simple search using the `portal_catalog` tool, which we look up from some context object. +We call the tool to perform a search, passing search criteria as keyword arguments, where the left hand side refers to an installed index, and the right hand side is the search term. + +Some of the more commonly used indexes are: + +`Title` +: The object's title. + +`Description` +: The object's description. + +`path` +: The object's path. + The argument is a string, such as `/foo/bar`. + To get the path of an object such as its parent folder, do `'/'.join(folder.getPhysicalPath())`. + Searching for an object's path will return the object and any children. + To limit the depth of the search, for example, to get only those paths one level deep, use a compound query, such as `path={'query': '/'.join(folder.getPhysicalPath()), 'depth': 1}`. + If `depth` is specified, the object at the given path is not returned, but any children within the depth limit are. + +`object_provides` +: Used to match interfaces provided by the object. + The argument is an interface name or list of interface names, of which any one may match. + To get the name of a given interface, you can call `ISomeInterface.__identifier__`. + +`portal_type` +: Used to match the portal type. + Note that users can rename portal types, so it is often better not to hardcode these. + Often using an `object_provides` search for a type-specific interface will be better. + Conversely, if you are asking the user to select a particular type to search for, then they should be choosing from the currently installed `portal_types`. + +`SearchableText` +: Used for full-text searches. + This supports operands such as `AND` and `OR` in the search string. + +`Creator` +: The username of the creator of a content item. + +`Subject` +: A `KeywordIndex` of object keywords. + +`review_state` +: An object's workflow state. + +In addition, the search results can be sorted based on any `FieldIndex`, `KeywordIndex`, or `DateIndex` using the following keyword arguments. + +- Use `sort_on=''` to sort on a particular index. + For example, `sort_on='sortable_title'` will produce a sensible title-based sort. + `sort_on='Date'` will sort on the publication date, or the creation date if this is not set. +- Add `sort_order='reverse'` to sort in reverse. + The default is `sort_order='ascending'`. + `'descending'` can be used as an alias for `'reverse'`. +- Add `sort_limit=10` to limit to approximately 10 search results. + Note that it is possible to get more results due to index optimizations. + Use a list slice on the catalog search results to be absolutely sure that you get the maximum number of results, for example, `results = catalog(…, sort_limit=10)[:10]`. + Also note that the use of `sort_limit` requires a `sort_on` as well. + +Some of the more commonly used metadata columns are the following. + +`Creator` +: The user who created the content object. + +`Date` +: The publication date or creation date, whichever is later. + +`Title` +: The object's title. + +`Description` +: The object's description. + +`getId` +: The object's ID. + Note that this is an attribute, not a function. + +`review_state` +: The object's workflow state. + +`portal_type` +: The object's portal type. + +For more information about catalog indexes and searching, see the +[ZCatalog chapter in the Zope 2 book](https://zope.readthedocs.io/en/latest/zopebook/SearchingZCatalog.html). + + +#### How to setup the index TTW: + +Now that the fields are indexible, we need to create the index itself. + +- Go to the Zope Management Interface. +- Go on {guilabel}`portal_catalog`. +- Click {guilabel}`Indexes` tab. +- There's a drop down menu to the top right to let you choose what type of index to add. + If you use a plain text string field, you would select {guilabel}`FieldIndex`. +- As the `id` put in the programmatical name of your Dexterity type field that you want to index. +- Hit {guilabel}`OK`, tick your new index, and click {guilabel}`Reindex`. + +You should now see content being indexed. + +See the [documentation on Indexes and Metadata](https://5.docs.plone.org/develop/plone/searching_and_indexing/indexing) for further information. diff --git a/plone.app.dexterity/advanced/custom-add-and-edit-forms.md b/plone.app.dexterity/advanced/custom-add-and-edit-forms.md new file mode 100644 index 000000000..bbf56be3a --- /dev/null +++ b/plone.app.dexterity/advanced/custom-add-and-edit-forms.md @@ -0,0 +1,173 @@ +--- +myst: + html_meta: + "description": "Custom add and edit forms in Plone" + "property=og:description": "Custom add and edit forms in Plone" + "property=og:title": "Custom add and edit forms in Plone" + "keywords": "Plone, custom, add, edit, forms" +--- + +# Custom add and edit forms + +This chapter describes how to use `z3c.form` to build custom forms. + +Until now, we have used Dexterity's default content add and edit forms, supplying form hints in our schemata to influence how the forms are built. +For most types, that is all that's ever needed. +In some cases, however, we want to build custom forms, or supply additional forms. + +Dexterity uses the [`z3c.form`](https://z3cform.readthedocs.io/en/latest/) library to build its forms, via the [`plone.z3cform`](https://pypi.org/project/plone.z3cform/) integration package. + +Dexterity also relies on [`plone.autoform`](https://pypi.org/project/plone.autoform/), in particular its `AutoExtensibleForm` base class, which is responsible for processing form hints and setting up `z3c.form` widgets and groups (fieldsets). +A custom form, therefore, is simply a view that uses these libraries, although Dexterity provides some helpful base classes that make it easier to construct forms based on the schema and behaviors of a Dexterity type. + +```{note} +If you want to build standalone forms not related to content objects, see the [`z3c.form` documentation](https://z3cform.readthedocs.io/en/latest/). +``` + + +## Edit forms + +An edit form is just a form that is registered for a particular type of content and knows how to register its fields. +If the form is named `edit`, it will replace the default edit form, which is registered with that name for the more general `IDexterityContent` interface. + +Dexterity provides a standard edit form base class that provides sensible defaults for buttons, labels, and so on. +This should be registered for a type schema (not a class). +To create an edit form that is identical to the default, we could do the following. + +```python +from plone.dexterity.browser import edit + +class EditForm(edit.DefaultEditForm): + pass +``` + +And register it in {file}`configure.zcml`. + +```xml + +``` + +This form is of course not terribly interesting, since it is identical to the default. +However, we can now start changing fields and values. +For example, we could do any of the following. + +- Override the `schema` property to tell `plone.autoform` to use a different schema interface (with different form hints) than the content type schema. +- Override the `additionalSchemata` property to tell `plone.autoform` to use different supplemental schema interfaces. + The default is to use all behavior interfaces that provide the `IFormFieldProvider` marker from `plone.autoform`. +- Override the `label` and `description` properties to provide a different title and description for the form. +- Set the `z3c.form` `fields` and `groups` attributes directly. +- Override the `updateWidgets()` method to modify widget properties, or one of the other `update()` methods, to perform additional processing on the fields. + In most cases, these require us to call the `super` version at the beginning. + See the [`plone.autoform`](https://pypi.org/project/plone.autoform/#introduction) and [`z3c.form` documentation](https://z3cform.readthedocs.io/en/latest/) to learn more about the sequence of calls that emanate from the form `update()` method in the `z3c.form.form.BaseForm` class. +- Override the `template` attribute to specify a custom template. + + +## Content add sequence + +Add forms are similar to edit forms in that they are built from a type's schema and the schemata of its behaviors. +However, for an add form to be able to construct a content object, it needs to know which `portal_type` to use. + +You should realize that the FTIs in the `portal_types` tool can be modified through the web. +It is even possible to create new types through the web that reuse existing classes and factories. + +For this reason, add forms are looked up via a namespace traversal adapter called `++add++`. +You may have noticed this in the URLs to add forms already. +What actually happens is the following. + +- Plone renders the {guilabel}`add` menu. + + - To do so, it looks, among other places, for actions in the `folder/add` category. + This category is provided by the `portal_types` tool. + - The `folder/add` action category is constructed by looking up the `add_view_expr` property on the FTIs of all addable types. + This is a TALES expression telling the add menu which URL to use. + - The default `add_view_expr` in Dexterity (and CMF 2.2) is `string:${folder_url}/++add++${fti/getId}`. + That is, it uses the `++add++` traversal namespace with an argument containing the FTI name. + +- A user clicks on an entry in the menu, and is taken to a URL using the parttern `/path/to/folder/++add++my.type`. + + - The `++add++` namespace adapter looks up the FTI with the given name, and gets its `factory` property. + - The `factory` property of an FTI gives the name of a particular `zope.component.interfaces.IFactory` utility, which is used later to construct an instance of the content object. + Dexterity automatically registers a factory instance for each type, with a name that matches the type name, although it is possible to use an existing factory name in a new type. + This allows administrators to create new "logical" types that are functionally identical to an existing type. + - The `++add++` namespace adapter looks up the actual form to render as a multi-adapter from `(context, request, fti)` to `Interface` with a name matching the `factory` property. + Recall that a standard view is a multi-adapter from `(context, request)` to `Interface` with a name matching the URL segment for which the view is looked up. + As such, add forms are not standard views, because they get the additional `fti` parameter when constructed. + - If this fails, there is no custom add form for this factory, as is normally the case. + The fallback is an unnamed adapter from `(context, request, fti)`. + The default Dexterity add form is registered as such an adapter, specific to the `IDexterityFTI` interface. + +- The form is rendered like any other `z3c.form` form instance, and is subject to validation, which may cause it to be loaded several times. + +- Eventually, the form is successfully submitted. + At this point: + + - The standard `AddForm` base class will look up the factory from the FTI reference it holds and call it to create an instance. + - The default Dexterity factory looks at the `klass` [^id2] attribute of the FTI to determine the actual content class to use, creates an object and initializes it. + - The `portal_type` attribute of the newly created instance is set to the name of the FTI. + Thus, if the FTI is a "logical type" created through the web, but using an existing factory, the new instance's `portal_type` will be set to the "logical type". + - The object is initialized with the values submitted in the form. + - An `IObjectCreatedEvent` is fired. + - The object is added to its container. + - The user is redirected to the view specified in the `immediate_view` property of the FTI. + +[^id2]: `class` is a reserved word in Python, so we use `klass`. + +This sequence is pretty long, but thankfully we rarely have to worry about it. +In most cases, we can use the default add form, and when we can't, creating a custom add form is only a bit more difficult than creating a custom edit form. + + +## Custom add forms + +As with edit forms, Dexterity provides a sensible base class for add forms that knows how to deal with the Dexterity FTI and factory. + +A custom form replicating the default would be the following. + +```python +from plone.dexterity.browser import add + +class AddForm(add.DefaultAddForm): + portal_type = 'example.fspage' + +class AddView(add.DefaultAddView): + form = AddForm +``` + +And be registered in ZCML as follows. + +```xml + + + + +``` + +The name here should match the *factory* name. +By default, Dexterity types have a factory called the same as the FTI name. +If no such factory exists (in other words, you have not registered a custom `IFactory` utility), a local factory utility will be created and managed by Dexterity when the FTI is installed. + +Also note that we do not specify a context here. +Add forms are always registered for any `IFolderish` context. + +```{note} +If the permission used for the add form is different from the `add_permission` set in the FTI, the user needs to have *both* permissions to be able to see the form and add content. +For this reason, most add forms will use the generic `cmf.AddPortalContent` permission. +The {guilabel}`add` menu will not render links to types where the user does not have the add permission stated in the FTI, even if this is different to `cmf.AddPortalContent`. +``` + +As with edit forms, we can customize this form by overriding `z3c.form` and `plone.autoform` properties and methods. +See the [`z3c.form` documentation](https://z3cform.readthedocs.io/en/latest/) on add forms for more details. diff --git a/plone.app.dexterity/advanced/custom-content-classes.md b/plone.app.dexterity/advanced/custom-content-classes.md new file mode 100644 index 000000000..145e542eb --- /dev/null +++ b/plone.app.dexterity/advanced/custom-content-classes.md @@ -0,0 +1,66 @@ +--- +myst: + html_meta: + "description": "How to add a custom content class implementation in Plone" + "property=og:description": "How to add a custom content class implementation in Plone" + "property=og:title": "How to add a custom content class implementation in Plone" + "keywords": "Plone, custom, content, class" +--- + +# Custom content classes + +This chapter describes how to add a custom content class implementation. + +When we learned about configuring the Dexterity FTI, we saw the `klass` attribute and how it could be used to refer to either the `Container` or `Item` content classes. +These classes are defined in the [`plone.dexterity.content`](https://github.com/plone/plone.dexterity/blob/master/plone/dexterity/content.py) module, and represent container (folder) and item (non-folder) types, respectively. + +For most applications, these two classes will suffice. +We will normally use behaviors, adapters, event handlers, and schema interfaces to build additional functionality for our types. +In some cases, however, it is useful or necessary to override the class, typically to override some method or property provided by the base class that cannot be implemented with an adapter override. +A custom class may also be able to provide marginally better performance by side-stepping some of the schema-dependent dynamic behavior found in the base classes. +In real life, you are very unlikely to notice, though. + +To create a custom class, derive from one of the standard ones, as shown below + +```python +from plone.dexterity.content import Item + +class MyItem(Item): + """A custom content class""" +``` + +For a container type, we'd do the following + +```python +from plone.dexterity.content import Container + +class MyContainer(Container): + """A custom content class""" +``` + +You can now add any required attributes or methods to this class. + +To make use of this class, set the `klass` attribute in the FTI to its dotted name, as shown in the following example. + +```xml +my.package.myitem.MyItem +``` + +This will cause the standard Dexterity factory to instantiate this class when the user submits the add form. + +```{note} +As an alternative to setting `klass` in the FTI, you may provide your own `IFactory` utility for this type in lieu of Dexterity's default factory (see [plone.dexterity.factory](https://github.com/plone/plone.dexterity/blob/master/plone/dexterity/factory.py)). +However, you need to be careful that this factory performs all necessary initialization, so it is normally better to use the standard factory. +``` + + +## Custom class caveats + +There are a few important caveats when working with custom content classes: + +- Make sure you use the correct base class: either `plone.dexterity.content.Item` or + `plone.dexterity.content.Container`. +- If you mix in other base classes, it is safer to put the `Item` or `Container` class first. + If another class comes first, it may override the `__name__`, `__providedBy__`, `__allow_access_to_unprotected_subobjects__`, or `isPrincipiaFolderish` properties, and possibly the `__getattr__()` and `__getitem__()` methods, causing problems with the dynamic schemata or folder item security. + In all cases, you may need to explicitly set these attributes to the ones from the correct base class. +- If you define a custom constructor, make sure it can be called with no arguments, and with an optional `id` argument giving the name. diff --git a/plone.app.dexterity/advanced/defaults.md b/plone.app.dexterity/advanced/defaults.md new file mode 100644 index 000000000..c5c98e546 --- /dev/null +++ b/plone.app.dexterity/advanced/defaults.md @@ -0,0 +1,67 @@ +--- +myst: + html_meta: + "description": "Content type defaults" + "property=og:description": "Content type defaults" + "property=og:title": "Content type defaults" + "keywords": "Plone, Content type, defaults" +--- + +# Defaults + +This chapter describes the default values for fields on add forms. + +It is often useful to calculate a default value for a field. +This value will be used on the add form before the field is set. + +To continue with our conference example, let's set the default values for the `start` and `end` dates to one week in the future and ten days in the future, respectively. +We can do this by adding the following to {file}`program.py`. + +```python +import datetime + +def startDefaultValue(): + return datetime.datetime.today() + datetime.timedelta(7) + +def endDefaultValue(): + return datetime.datetime.today() + datetime.timedelta(10) +``` + +We also need to modify `IProgram` so the `start` and `end` fields use these functions as their `defaultFactory`. + +```python +class IProgram(model.Schema): + + start = schema.Datetime( + title=_("Start date"), + required=False, + defaultFactory=startDefaultValue, + ) + + end = schema.Datetime( + title=_("End date"), + required=False, + defaultFactory=endDefaultValue, + ) +``` + +The `defaultFactory` is a function that will be called when the add form is loaded to determine the default value. + +The value returned by the method should be a value that's allowable for the field. +In the case of `Datetime` fields, that's a Python `datetime` object. + +It is also possible to write a context-aware default factory that will be passed the container for which the add form is being displayed. + +```python +from zope.interface import provider +from zope.schema.interfaces import IContextAwareDefaultFactory + +@provider(IContextAwareDefaultFactory) +def getContainerId(context): + return context.getId() +``` + +It is possible to provide different default values depending on the type of context, a request layer, the type of form, or the type of widget used. +See the [z3c.form](https://z3cform.readthedocs.io/en/latest/advanced/validator.html#look-up-value-from-default-adapter) documentation for more details. + +We'll cover creating custom forms later in this manual. diff --git a/plone.app.dexterity/advanced/event-handlers.md b/plone.app.dexterity/advanced/event-handlers.md new file mode 100644 index 000000000..0ae5d8da8 --- /dev/null +++ b/plone.app.dexterity/advanced/event-handlers.md @@ -0,0 +1,122 @@ +--- +myst: + html_meta: + "description": "How to add custom event handlers for your content types" + "property=og:description": "How to add custom event handlers for your content types" + "property=og:title": "How to add custom event handlers for your content types" + "keywords": "Plone, content types, event handlers" +--- + +# Event handlers + +This chapter describes how to add custom event handlers for your type. + +Zope (and so Plone) has a powerful event notification and subscriber subsystem. +Events notifications are already fired at several places. + +With custom subscribers to these events, more dynamic functionality can be added. +It is possible to react when something happens to objects of a specific type. + +Zope's event model is *synchronous*. +When an event is broadcast (via the `notify()` function from the [`zope.event`](https://pypi.org/project/zope.event/) package), all registered event handlers will be called. +This happens, for example, from the `save` action of an add form, or on move or delete of content objects. +There is no guarantee in which order the event handlers will be called. + +Each event is described by an interface, and will typically carry some information about the event. +Some events are known as *object events*, and provide `zope.component.interfaces.IObjectEvent`. +These have an `object` attribute giving access to the content object that the event relates to. +Object events allow event handlers to be registered for a specific type of object as well as a specific type of event. + +Some of the most commonly used event types in Plone are shown below. +They are all object events. + +`zope.lifecycleevent.interfaces.IObjectCreatedEvent` +: Fired by the standard add form just after an object has been created, but before it has been added on the container. + Note that it is often easier to write a handler for `IObjectAddedEvent` (see below), because at this point the object has a proper acquisition context. + +`zope.lifecycleevent.interfaces.IObjectAddedEvent` +: Fired when an object has been added to its container. + The container is available as the `newParent` attribute. + The name the new item holds in the container is available as `newName`. + +`OFS.interfaces.IObjectWillBeAddedEvent` +: Fired before an object is added to its container. + It is also fired on move of an object (copy/paste). + +`zope.lifecycleevent.interfaces.IObjectModifiedEvent` +: Fired by the standard edit form when an object has been modified. + +`zope.lifecycleevent.interfaces.IObjectRemovedEvent` +: Fired when an object has been removed from its container. + The container is available as the `oldParent` attribute. + The name the item held in the container is available as `oldName`. + +`OFS.interfaces.IObjectWillBeRemovedEvent` +: Fired before an object is removed. + Until here, no deletion has happend. + It is also fired on move of an object (copy/paste). + +`zope.lifecycleevent.interfaces.IObjectMovedEvent` +: Fired when an object is added to, removed from, renamed in, or moved between containers. + This event is a super-type of `IObjectAddedEvent` and `IObjectRemovedEvent`, shown above. + An event handler registered for this interface will be invoked for the "added" and "removed" cases as well. + When an object is moved or renamed, all of `oldParent`, `newParent`, `oldName`, and `newName` will be set. + +`Products.CMFCore.interfaces.IActionSucceededEvent` +: Fired when a workflow event has completed. + The `workflow` attribute holds the workflow instance involved, and the `action` attribute holds the action (transition) invoked. + +Event handlers can be registered using ZCML with the `` directive. + +As an example, let's add an event handler to the `Presenter` type. +It tries to find users with matching names matching the presenter ID, and sends these users an email. + +First, we require an additional import at the top of {file}`presenter.py`. + +```python +from plone import api +``` + +Then we'll add the following event subscriber after the schema definition. + +```python +def notifyUser(presenter, event): + acl_users = api.portal.get_tool("acl_users") + sender = api.portal.get_registry_record("plone.email_from_name") + + if not sender: + return + + subject = "Is this you?" + message = "A presenter called {0} was added here {1}".format( + presenter.title, + presenter.absolute_url() + ) + + matching_users = acl_users.searchUsers(fullname=presenter.title) + for user_info in matching_users: + email = user_info.get("email", None) + if email is not None: + api.portal.send_email( + recipient=email, + sender=sender, + subject=subject + body=message, + ) +``` + +And register it in ZCML. + +- The first argument to `for` is an interface describing the object type. +- The second argument is the event type. +- The arguments to the function reflects these two, so the first argument is the `IPresenter` instance, and the second is an `IObjectAddedEvent` instance. + +```xml + +``` + +There are many ways to improve this rather simplistic event handler, but it illustrates how events can be used. diff --git a/plone.app.dexterity/advanced/files-and-images.md b/plone.app.dexterity/advanced/files-and-images.md new file mode 100644 index 000000000..e7b0d1b25 --- /dev/null +++ b/plone.app.dexterity/advanced/files-and-images.md @@ -0,0 +1,111 @@ +--- +myst: + html_meta: + "description": "How to work with file and image fields, including blobs, in Plone content types" + "property=og:description": "How to work with file and image fields, including blobs, in Plone content types" + "property=og:title": "How to work with file and image fields, including blobs, in Plone content types" + "keywords": "Plone, content types, files, images, blobs" +--- + +# Files and images + +This chapter describes how to work with file and image fields, including blobs. + +Plone has dedicated `File` and `Image` types, and it is often preferable to use these for managing files and images. +However, it is sometimes useful to treat fields on an object as binary data. +When working with Dexterity, you can accomplish this by using [`plone.namedfile`](https://pypi.org/project/plone.namedfile/) and [`plone.formwidget.namedfile`](https://pypi.org/project/plone.formwidget.namedfile/). + +The `plone.namedfile` package includes four field types, all found in the `plone.namedfile.field` module. + +`NamedFile` +: Stores non-blob files. + This is useful for small files when you don't want to configure blob storage. + +`NamedImage` + Stores non-blob images. + +`NamedBlobFile` +: Stores blob files (see note below). + It is otherwise identical to `NamedFile`. + +`NamedBlobImage` +: Stores blob images (see note below). + It is otherwise identical to `NamedImage`. + +In use, the four field types are all pretty similar. +They actually store persistent objects of type `plone.namedfile.NamedFile`, `plone.namedfile.NamedImage`, `plone.namedfile.NamedBlobFile` and `plone.namedfile.NamedBlobImage`, respectively. +Note the different module! +These objects have attributes, such as `data` to access the raw binary data, `contentType`, to get a MIME type, and `filename` to get the original filename. +The image values also support `_height` and `_width` to get image dimensions. + +To use the non-blob image and file fields, it is sufficient to depend on `plone.formwidget.namedfile`, since this includes `plone.namefile` as a dependency. +We prefer to be explicit in `setup.py`, however, since we will actually import it directly from `plone.namedfile`. + +```ini +install_requires=[ + "plone.namedfile", + "plone.formwidget.namedfile", +], +``` + +```{note} +Again, we do not need separate `` lines in `configure.zcml` for these new dependencies, because we use ``. +``` + +To demonstrate, we will add an image of the speaker to the `Presenter` type. +In {file}`presenter.py`, we add the following. + +```python +from plone.namedfile.field import NamedBlobImage + +class IPresenter(model.Schema): + + picture = NamedBlobImage( + title=_("Please upload an image"), + required=False, + ) +``` + +To use this in a view, we can either use a display widget via a `DisplayForm`, or construct a download URL manually. +Since we don't have a `DisplayForm` for the `Presenter` type, we'll do the latter. +Of course, we could easily turn the view into a display form as well. + +In {file}`presenter_templates/view.pt`, we add the following block of TAL. + +```xml +
+ +
+``` + +This constructs an image URL using the `@@download` view from `plone.namedfile`. +This view takes the name of the field containing the file or image on the traversal subpath (`/picture`), and optionally a filename on a further sub-path. +The filename is used mainly so that the URL ends in the correct extension, which can help ensure web browsers display the picture correctly. +We also define the `height` and `width` of the image based on the values set on the object. + +Access to image scales is similar, as shown below. + +```xml +
+ +
+``` + +Where `scales` is either `large`, `preview`, `mini`, `thumb`, `tile`, `icon`, or a custom scale. +This code generates a full tag, including height, width, alt, and title attributes based on the context title. +To generate just a URL, use code as shown: + +```xml + +``` + +For file fields, you can construct a download URL in a similar way, using an `` tag. + +```xml + +``` diff --git a/plone.app.dexterity/advanced/index.md b/plone.app.dexterity/advanced/index.md new file mode 100644 index 000000000..bb2b6d043 --- /dev/null +++ b/plone.app.dexterity/advanced/index.md @@ -0,0 +1,32 @@ +--- +myst: + html_meta: + "description": "Advanced configuration of Plone content types" + "property=og:description": "Advanced configuration of Plone content types" + "property=og:title": "Advanced configuration of Plone content types" + "keywords": "Plone, advanced, configuration, Dexterity, content types" +--- + +# Advanced configuration + +This part describes further configuration and tips for content types in Plone. + +```{toctree} +:maxdepth: 2 + +defaults +validators +vocabularies +references +rich-text-markup-transformations +files-and-images +static-resources +behaviors +event-handlers +permissions +workflow +catalog-indexing-strategies +custom-add-and-edit-forms +custom-content-classes +webdav-and-other-file-representations +``` diff --git a/plone.app.dexterity/advanced/permissions.md b/plone.app.dexterity/advanced/permissions.md new file mode 100644 index 000000000..e181d7983 --- /dev/null +++ b/plone.app.dexterity/advanced/permissions.md @@ -0,0 +1,178 @@ +--- +myst: + html_meta: + "description": "How to set up add permissions, view permissions, and field view/edit permissions for Plone content types" + "property=og:description": "How to set up add permissions, view permissions, and field view/edit permissions for Plone content types" + "property=og:title": "How to set up add permissions, view permissions, and field view/edit permissions for Plone content types" + "keywords": "Plone, Permissions, content types" +--- + +# Permissions + +This chapter describes how to set up add permissions, view permissions, and field view/edit permissions. + +Plone's security system is based on the concept of *permissions* protecting *operations*. +These operations include accessing a view, viewing a field, modifying a field, or adding a type of content. +Permissions are granted to *roles*, which in turn are granted to *users* or *groups*. +In the context of developing content types, permissions are typically used in three different ways. + +- A content type or group of related content types often has a custom *add permission* which controls who can add this type of content. +- Views (including forms) are sometimes protected by custom permissions. +- Individual fields are sometimes protected by permissions, so that some users can view and edit fields that others can't see. + +It is easy to create new permissions. +However, be aware that it is considered good practice to use the standard permissions wherever possible and use *workflow* to control which roles are granted these permissions on a per-instance basis. + +For more basic information on permissions and how to create custom permissions read the [Security Section](https://5.docs.plone.org/develop/plone/security/index.html) in the Plone documentation. + + +## Performing permission checks in code + +It is sometimes necessary to check permissions explicitly in code, for example, in a view. +A permission check always checks a permission on a context object, since permissions can change with workflow. + +```{note} +Never make security dependent on users' roles directly. +Always check for a permission, and assign the permission to the appropriate role or roles. +``` + +As an example, let's display a message on the view of a `Session` type if the user has the `cmf.RequestReview` permission. +In {file}`session.py`, we update the `View` class with the following. + +```python +from zope.security import checkPermission + +class View(BrowserView): + + def canRequestReview(self): + return checkPermission('cmf.RequestReview', self.context) +``` + +And in the {file}`session_templates/view.pt` template, we add the following. + +```xml +
+ Please submit this for review. +
+``` + + +## Content type add permissions + +Dexterity content types' add permissions are set in the FTI, using the `add_permission` property. +This can be changed through the web or in the GenericSetup import step for the content type. + +To make the `Session` type use our new permission, we modify the `add_permission` line in {file}`profiles/default/example.conference.session.xml`. + +```xml +example.conference.AddSession +``` + + +## Protecting views and forms + +Access to views and other browser resources such as viewlets or portlets can be protected by permissions, either using the `permission` attribute on ZCML statements such as the following + +```xml + +``` + +We could also use the special `zope.Public` permission name to make the view accessible to anyone. + + +## Protecting form fields + +Individual fields in a schema may be associated with a *read* permission and a *write* permission. +The read permission is used to control access to the field's value via protected code, such as scripts or templates created through the web, and URL traversal. +It can be used to control the appearance of fields when using display forms. +If you use custom views that access the attribute directly, you'll need to perform your own checks. +Write permissions can be used to control whether or not a given field appears on a type's add and edit forms. + +In both cases, read and write permissions are annotated onto the schema using directives similar to those we've already seen for form widget hints. +The `read_permission()` and `write_permission()` directives are found in the [`plone.autoform`](https://pypi.org/project/plone.autoform/) package. + +If XML schemas are used for definitions, see [Dexterity XML, supermodel/security attributes](https://5.docs.plone.org/external/plone.app.dexterity/docs/reference/dexterity-xml.html#supermodel-security-attributes). + +The following example protects a field to be readable for Site Administrators only. + +```python +from zope import schema +from plone.supermodel import model +from plone.autoform.directives import read_permission, write_permission + +class IExampleProtectedInformation(model): + + read_permission(info='cmf.ManagePortal') + write_permission(info='cmf.ManagePortal') + info = schema.Text( + title=_("Information"), + ) +``` + +As a complex example, let's add a field for *Session* reviewers to record the track for a session. +We'll store the vocabulary of available tracks on the parent `Program` object in a text field, so that the creator of the `Program` can choose the available tracks. + +First, we add this to the `IProgram` schema in {file}`program.py`. + +```python +form.widget(tracks=TextLinesFieldWidget) +tracks = schema.List( + title=_("Tracks"), + required=True, + default=[], + value_type=schema.TextLine(), + ) +``` + +The `TextLinesFieldWidget` is used to edit a list of text lines in a text area. +It is imported as shown. + +```python +from plone.z3cform.textlines.textlines import TextLinesFieldWidget +``` + +Next, we'll add a vocabulary for this to {file}`session.py`: + +```python +from Acquisition import aq_inner, aq_parent +from zope.component import provider +from zope.schema.interfaces import IContextSourceBinder +from zope.schema.vocabulary import SimpleVocabulary + + +@provider(IContextSourceBinder) +def possibleTracks(context): + + # we put the import here to avoid a circular import + from example.conference.program import IProgram + while context is not None and not IProgram.providedBy(context): + context = aq_parent(aq_inner(context)) + + values = [] + if context is not None and context.tracks: + values = context.tracks + + return SimpleVocabulary.fromValues(values) +``` + +This vocabulary finds the closest `IProgram`. +In the add form, the `context` will be the `Program`, but on the edit form, it will be the `Session`, so we need to check the parent. +It uses its `tracks` variable as the vocabulary. + +Next, we add a field to the `ISession` interface in the same file and protect it with the relevant write permission. + +```python +write_permission(track='example.conference.ModifyTrack') +track = schema.Choice( + title=_("Track"), + source=possibleTracks, + required=False, + ) +``` + +With this in place, users with the `example.conference: Modify track` permission should be able to edit tracks for a session. +For everyone else, the field will be hidden in the edit form. diff --git a/plone.app.dexterity/advanced/references.md b/plone.app.dexterity/advanced/references.md new file mode 100644 index 000000000..08ba92296 --- /dev/null +++ b/plone.app.dexterity/advanced/references.md @@ -0,0 +1,152 @@ +--- +myst: + html_meta: + "description": "Plone content type references between content objects" + "property=og:description": "Plone content type references between content objects" + "property=og:title": "Plone content type references between content objects" + "keywords": "Plone, content types, references, content objects" +--- + +# References + +This chapter describes how to work with references between content objects. + +References are a way to maintain links between content that remain valid, even if one or both of the linked items are moved or renamed. + +Under the hood, Dexterity's reference system uses [`five.intid`](https://pypi.org/project/five.intid/), a Zope 2 integration layer for [`zope.intid`](https://pypi.org/project/zope.intid/), to give each content item a unique integer ID. +These are the bases for relationships maintained with the [`zc.relationship`](https://pypi.org/project/zc.relationship/) package, which in turn is accessed via an API provided by [`z3c.relationfield`](https://pypi.org/project/z3c.relationfield/), integrated into Zope 2 with [`plone.app.relationfield`](https://pypi.org/project/plone.app.relationfield/). +For most purposes, you need only to worry about the `z3c.relationfield` API, which provides methods for finding source and target objects for references and searching the relationship catalog. + +References are most commonly used in form fields with a selection or content browser widget. +Dexterity comes with a standard widget in [`plone.formwidget.contenttree`](https://pypi.org/project/plone.formwidget.contenttree/) configured for the `RelationList` and `RelationChoice` fields from `z3c.relationfield`. + +To illustrate the use of references, we will allow the user to create a link between a `Session` and its `Presenter`. +Since Dexterity already ships with and installs `plone.formwidget.contenttree` and `z3c.relationfield`, we do not need to add any further setup code, and we can use the field directly in {file}`session.py`: + +```python +from z3c.relationfield.schema import RelationChoice +from plone.formwidget.contenttree import ObjPathSourceBinder + +from example.conference.presenter import IPresenter + +class ISession(form.Schema): + """A conference session. Sessions are managed inside Programs. + """ + + presenter = RelationChoice( + title=_("Presenter"), + source=ObjPathSourceBinder(object_provides=IPresenter.__identifier__), + required=False, + ) +``` + +```{note} +Remeber that `plone.app.relationfield` needs to be installed to use any `RelationChoice` or `RelationList` field. +``` + +To allow multiple items to be selected, we could have used a `RelationList` as shown: + +```python +relatedItems = RelationList( + title="Related Items", + default=[], + value_type=RelationChoice(title=_("Related"), + source=ObjPathSourceBinder()), + required=False, +) +``` + +The `ObjPathSourceBinder` class is an `IContextSourceBinder` that returns a vocabulary with content objects as values, object titles as term titles, and object paths as tokens. + +You can pass keyword arguments to the constructor for `ObjPathSourceBinder()` to restrict the selectable objects. +Here we demand that the object must provide the `IPresenter` interface. +The syntax is the same as that used in a catalog search, except that only simple values and lists are allowed (in other words, you can't use a dict to specify a range or values for a field index). + +If you want to restrict the folders and other content shown in the content browser, you can pass a dictionary with catalog search parameters (and here, any valid catalog query will do) as the first non-keyword argument (`navigation_tree_query`) to the `ObjPathSourceBinder()` constructor. + +You can also create the fields in an XML schema. +However you have to provide a pre-baked source instance. +If you are happy with not restricting folders shown, you can use those which `plone.formwidget.contenttree` makes for you. +For example: + +```xml + + Related Items + + Related + plone.formwidget.contenttree.obj_path_src_binder + + +``` + +```{versionadded} 4.3.2 +The pre-baked source binders were added in `plone.formwidget.contenttree` 1.0.7, which ships with Plone 4.3.2+. +``` + +If you want to use a different widget, you can use the same source (or a custom source that has content objects as values) with something such as the autocomplete widget. +The following line added to the interface will make the presenter selection similar to the `organizer` selection widget that we showed in the previous section. + +```python +from plone.autoform import directives +directives.widget('presenter', AutocompleteFieldWidget) +``` + +Once the user has created some relationships, the value stored in the relation field is a `RelationValue` object. +This provides various attributes, including: + +`from_object` +: The object from which the relationship is made. + +`to_object` +: The object to which the relationship is made. + +`from_id` and `to_id` +: The integer IDs of the source and target. + +`from_path` and `to_path` +: The path of the source and target. + +The `isBroken()` method can be used to determine if the relationship is broken. +This normally happens if the target object is deleted. + +To display the relationship on our form, we can either use a display widget on a *display view*, or use this API to find the object and display it. +We'll do the latter in {file}`templates/sessionview.pt`. + +```xml +
+ + +
+``` + + +## Back references + +To retrieve back references (all objects pointing to a particular object using the specified attribute) you can't simply use `from_object` or `from_path`, because the source object is stored in the relation without acquisition wrappers. +You should use the `from_id` method, which searches the object in the `IntId` catalog: + +```python +from Acquisition import aq_inner +from zope.component import getUtility +from zope.intid.interfaces import IIntIds +from zope.security import checkPermission +from zc.relation.interfaces import ICatalog + +def back_references(source_object, attribute_name): + """ + Return back references from source object on specified attribute_name + """ + catalog = getUtility(ICatalog) + intids = getUtility(IIntIds) + result = [] + for rel in catalog.findRelations( + dict(to_id=intids.getId(aq_inner(source_object)), + from_attribute=attribute_name) + ): + obj = intids.queryObject(rel.from_id) + if obj is not None and checkPermission('zope2.View', obj): + result.append(obj) + return result +``` + +This method does not check effective and expiration dates or content language. diff --git a/plone.app.dexterity/advanced/rich-text-markup-transformations.md b/plone.app.dexterity/advanced/rich-text-markup-transformations.md new file mode 100644 index 000000000..15c58e99c --- /dev/null +++ b/plone.app.dexterity/advanced/rich-text-markup-transformations.md @@ -0,0 +1,193 @@ +--- +myst: + html_meta: + "description": "How to store markup (such as HTML or reStructuredText) and render it with a transformation" + "property=og:description": "How to store markup (such as HTML or reStructuredText) and render it with a transformation" + "property=og:title": "How to store markup (such as HTML or reStructuredText) and render it with a transformation" + "keywords": "Plone, content types, rich text, markup, and transformations" +--- + +# Rich text, markup, and transformations + +This chapter describes how to store markup (such as HTML or reStructuredText) and render it with a transformation. + +Many content items need to allow users to provide rich text in some kind of markup, be that HTML (perhaps entered using a "What You See Is What You Get" or WYSIWYG editor), reStructuredText, Markdown, or some other format. +This markup typically needs to be transformed into HTML for the view template, but we also want to keep track of the original raw markup so that it can be edited again. +Even when the input format is HTML, there is often a need for a transformation to tidy up the HTML and strip out tags that are not permitted. + +It is possible to store HTML in a standard `Text` field. +You can even get a WYSIWYG widget, by using a schema such as the following. + +```python +from plone.autoform import directives as form +from plone.supermodel import model +from zope import schema +from plone.app.z3cform.wysiwyg import WysiwygFieldWidget + +class ITestSchema(model.Schema): + + form.widget('body', WysiwygFieldWidget) + body = schema.Text(title="Body text") +``` + +(richtext-label)= + +However, this approach does not allow for alternative markups or any form of content filtering. +For that we need to use a more powerful field, `RichText`, from the [`plone.app.textfield`](https://pypi.org/project/plone.app.textfield/) package. + +```python +from plone.app.textfield import RichText +from plone.supermodel import model + +class ITestSchema(model.Schema): + + body = RichText(title="Body text") +``` + +The `RichText` field constructor can take the following arguments, in addition to the usual arguments for a `Text` field. + +`default_mime_type` +: A string representing the default MIME type of the input markup. + This defaults to `text/html`. + +`output_mime_type` +: A string representing the default output MIME type. + This defaults to `text/x-html-safe`, which is a Plone-specific MIME type that disallows certain tags. + Use the {guilabel}`HTML Filtering` control panel in Plone to control the tags. + +`allowed_mime_types` +: A tuple of strings giving a vocabulary of allowed input MIME types. + If this is `None` (the default), the allowable types will be restricted to those set in Plone's {guilabel}`Markup` control panel. + +The *default* field can be set to either a Unicode object (in which case it will be assumed to be a string of the default MIME type) or a `RichTextValue` object (see below). + +Below is an example of a field that allows StructuredText and reStructuredText, transformed to HTML by default. + +```python +from plone.app.textfield import RichText +from plone.supermodel import model + +defaultBody = """\ +Background +========== + +Please fill this in + +Details +======= + +And this +""" + +class ITestSchema(model.Schema): + + body = RichText( + title="Body text", + default_mime_type='text/x-rst', + output_mime_type='text/x-html', + allowed_mime_types=('text/x-rst', 'text/structured',), + default=defaultBody, + ) +``` + + +## The RichTextValue + +The `RichText` field does not store a string. +Instead, it stores a `RichTextValue` object. +This is an immutable object that has the following properties. + +`raw` +: A Unicode string with the original input markup. + +`mimeType` +: The MIME type of the original markup, for example `text/html` or `text/structured`. + +`encoding` +: The default character encoding used when transforming the input markup. + Most likely, this will be UTF-8. + +`raw_encoded` +: The raw input encoded in the given encoding. + +`outputMimeType` +: The MIME type of the default output, taken from the field at the time of instantiation. + +`output` +: A Unicode object representing the transformed output. + If possible, this is cached persistently, until the `RichTextValue` is replaced with a new one (as happens when an edit form is saved, for example). + +The storage of the `RichTextValue` object is optimized for the case where the transformed output will be read frequently (for example, on the view screen of the content object) and the raw value will be read infrequently (for example, on the edit screen). +Because the output value is cached indefinitely, you will need to replace the `RichTextValue` object with a new one if any of the transformation parameters change. +However, as we will see below, it is possible to apply a different transformation on demand, if you need to. + +The code snippet belows shows how a `RichTextValue` object can be constructed in code. +In this case, we have a raw input string of type `text/plain` that will be transformed to a default output of `text/html`. +Note that we would normally look up the default output type from the field instance. + +```python +from plone.app.textfield.value import RichTextValue + +context.body = RichTextValue("Some input text", 'text/plain', 'text/html') +``` + +Of course, the standard widget used for a `RichText` field will correctly store this type of object for you, so it is rarely necessary to create one yourself. + + +## Using rich text fields in templates + +What about using the text field in a template? +If you use a `DisplayForm`, the display widget for the `RichText` field will render the transformed output markup automatically. +If you write TAL manually, you may try something like the following. + +```xml +
+``` + +This, however, will render a string as follows. + +```html +RichTextValue object. (Did you mean .raw or .output?) +``` + +The correct syntax is: + +```xml +
+``` + +This will render the cached, transformed output. +This operation is approximately as efficient as rendering a simple `Text` field, since the transformation is only applied once, when the value is first saved. + + +## Alternative transformations + +Sometimes, you may want to invoke alternative transformations. +Under the hood, the default implementation uses the `portal_transforms` tool to calculate a transform chain from the raw value's input MIME type to the desired output MIME type. +If you need to write your own transforms, take a look at [this tutorial](https://5.docs.plone.org/develop/plone/misc/portal_transforms.html). +This is abstracted behind an `ITransformer` adapter to allow alternative implementations. + +To invoke a transformation in code, you can use the following syntax. + +```python +from plone.app.textfield.interfaces import ITransformer + +transformer = ITransformer(context) +transformedValue = transformer(context.body, 'text/plain') +``` + +The `__call__()` method of the `ITransformer` adapter takes a `RichTextValue` object and an output MIME type as parameters. + +If you write a page template, there is an even more convenient syntax. + +```xml +
+``` + +The first traversal name gives the name of the field on the context (`body` in this case). +The second and third give the output MIME type. +If the MIME type is omitted, the default output MIME type will be used. + +```{note} +Unlike the `output` property, the value is not cached, and so will be calculated each time the page is rendered. +``` diff --git a/plone.app.dexterity/advanced/static-resources.md b/plone.app.dexterity/advanced/static-resources.md new file mode 100644 index 000000000..c9080a888 --- /dev/null +++ b/plone.app.dexterity/advanced/static-resources.md @@ -0,0 +1,120 @@ +--- +myst: + html_meta: + "description": "How to add images, stylesheets, JavaScripts, and other static assets with Plone content types" + "property=og:description": "How to add images, stylesheets, JavaScripts, and other static assets with Plone content types" + "property=og:title": "How to add images, stylesheets, JavaScripts, and other static assets with Plone content types" + "keywords": "Plone, content types, static, resources" +--- + +# Static resources + +This chapter describes how to add images, stylesheets, JavaScripts, and other static assets. + +Earlier in this manual, we have seen how to create views, and how to use file and image fields. +These are all dynamic, however, and often we just want to ship with a static image, icon, CSS or JavaScript file. +For this, we need to register static resources. + + +## Registering a static resource directory + +The easiest way to manage static resources is to create a `static` resource directory in your Dexterity project using the ZCML `resourceDirectory` directive. + +Registration of the resource directory is done using the `` ZCML directive. +This requires two attributes. +`name` is the name that appears after the `++resource++` namespace +`directory` is a relative path to the directory containing resources. + +It's conventional to use `static` for the directory name, and the dotted name of your package for the resource name. +You would use this ZCML to register it. + +```xml + +``` + +Then, if a "static" resource directory in the `example.conference` package contains a file called {file}`conference.css`, it will be accessible on a URL, such as `https:///site/++resource++example.conference/conference.css`. +The resource name is the same as the package name wherein the `resources` directory appears. + + +## Importing CSS and JavaScript files in templates + +One common use of static resources is to add a static CSS or JavaScript file to a specific template. +We can do this by filling the `style_slot` or `javascript_slot` in Plone's `main_template` in our own view template and using an appropriate resource link. + +For example, we could add the following near the top of {file}`presenter_templates/view.pt`. + +```xml + + + + + +``` + +```{note} +Always create the resource URL relative to the navigation root as shown here, so that the URL is the same for all content objects using this view. +This allows for efficient resource caching. +``` + + +## Registering resources with Plone's resource registries + +Sometimes it is more appropriate to register a stylesheet with Plone's `portal_css` registry (or a JavaScript file with `portal_javascripts`), rather than add the registration on a per-template basis. +This ensures that the resource is available site-wide. + +```{note} +It may seem wasteful to include a resource that is not be used on all pages in the global registry. +Remember, however, that `portal_css` and `portal_javascripts` will merge and compress resources, and set caching headers such that browsers and caching proxies can cache resources well. +It is often more effective to have one slightly larger file that caches well, than to have a variable number of files that may need to be loaded at different times. +``` + +To add a static resource file, you can use the GenericSetup {file}`cssregistry.xml` or {file}`jsregistry.xml` import steps in the `profiles/default` directory. For example, an import step to add the {file}`conference.css` file site-wide may involve a {file}`cssregistry.xml` file such as the following. + +```xml + + + + +``` + +Similarly, a JavaScript resource could be imported with a {file}`jsregistry.xml` file such as the following. + +```xml + + + + +``` + + +## Image resources + +Images can be added to resource directories just like any other type of resource. +To use the image in a view, you can construct an `` tag as follows. + +```xml + +``` + + +## Content type icons + +Finally, to use an image resource as the icon for a content type, simply list it in the FTI under the `content_icon` property. +For example, in {file}`profiles/default/types/example.conference.presenter.xml`, we can use the following line, presuming we have a {file}`presenter.gif` in the `example.conference` resource directory. + +```xml +++resource++example.conference/presenter.gif +``` diff --git a/plone.app.dexterity/advanced/validators.md b/plone.app.dexterity/advanced/validators.md new file mode 100644 index 000000000..be134c604 --- /dev/null +++ b/plone.app.dexterity/advanced/validators.md @@ -0,0 +1,98 @@ +--- +myst: + html_meta: + "description": "Validators" + "property=og:description": "Validators" + "property=og:title": "Validators" + "keywords": "Plone, Validators, constraints, content types" +--- + +# Validators + +This chapter describes how to create custom validators for your type. + +Many applications require some form of data entry validation. +The simplest form of validation you get for free, the [z3c.form](https://pypi.org/project/z3c.form/) library ensures that all data entered in Dexterity add and edit forms is valid for the field type. + +It is also possible to set certain properties on the fields to add further validation, or even create your own fields with custom validation logic, although that is a lot less common. +These properties are set as parameters to the field constructor when the schema interface is created. +You should see the [zope.schema](https://pypi.org/project/zope.schema/) package for details. + +The most common constraints are: + +`required=True/False` +: to make a field required or optional; + +`min` and `max` +: used for `Int`, `Float`, `Datetime`, `Date`, and `Timedelta` fields, specify the minimum and maximum (inclusive) allowed values of the given type. + +`min_length` and `max_length` +: used for collection fields (`Tuple`, `List`, `Set`, `Frozenset`, `Dict`) and text fields (`Bytes`, `BytesLine`, `ASCII`, `ASCIILine`, `Text`, `TextLine`), set the minimum and maximum (inclusive) length of a field. + + +## Constraints + +If this does not suffice, you can pass your own constraint function to a field. +The constraint function should take a single argument: the value that is to be validated. +This will be the field's type. +The function should return a boolean `True` or `False`. + +```python +def checkForMagic(value): + return 'magic' in value +``` + +```{note} +The constraint function does not have access to the context, but if you need to acquire a tool, you can use the `zope.component.hooks.getSite()` method to obtain the site root. +``` + +To use the constraint, pass the function as the `constraint` argument to the field constructor, for example: + +```python +my_field = schema.TextLine(title=_("My field"), constraint=checkForMagic) +``` + +Constraints are easy to write, but do not necessarily produce very friendly error messages. +It is possible to customize these error messages using `z3c.form` error view snippets. +See the [z3c.form documentation](https://z3cform.readthedocs.io/en/latest/) for more details. + + +## Invariants + +You'll also notice that constraints only check a single field value. +If you need to write a validator that compares multiple values, you can use an invariant. +Invariants use exceptions to signal errors, which are displayed at the top of the form rather than next to a particular field. + +To illustrate an invariant, let's make sure that the start date of a `Program` is before the end date. +In `program.py`, we add the following. + +```python +from zope.interface import invariant, Invalid + +class StartBeforeEnd(Invalid): + __doc__ = _("The start or end date is invalid") + +class IProgram(model.Schema): + + start = schema.Datetime( + title=_("Start date"), + required=False, + ) + + end = schema.Datetime( + title=_("End date"), + required=False, + ) + + @invariant + def validateStartEnd(data): + if data.start is not None and data.end is not None: + if data.start > data.end: + raise StartBeforeEnd(_("The start date must be before the end date.")) +``` + + +## Form validators + +Finally, you can write more powerful validators by using the `z3c.form` widget validators. +For details see [`z3c.form` validators](https://5.docs.plone.org/develop/plone/forms/z3c.form.html#validators). diff --git a/plone.app.dexterity/advanced/vocabularies.md b/plone.app.dexterity/advanced/vocabularies.md new file mode 100644 index 000000000..0fa729730 --- /dev/null +++ b/plone.app.dexterity/advanced/vocabularies.md @@ -0,0 +1,326 @@ +--- +myst: + html_meta: + "description": "Vocabularies" + "property=og:description": "Vocabularies" + "property=og:title": "Vocabularies" + "keywords": "" +--- + +# Vocabularies + +This chapter describes how to create your own static and dynamic vocabularies. + +Vocabularies are normally used in conjunction with selection fields, and are supported by the [`zope.schema`](https://pypi.org/project/zope.schema/) package, with widgets provided by [`z3c.form`](https://pypi.org/project/z3c.form/). + +Selection fields use the `Choice` field type. +To allow the user to select a single value, use a `Choice` field directly: + +```python +class IMySchema(model.Schema): + myChoice = schema.Choice(...) +``` + +For a multi-select field, use a `List`, `Tuple`, `Set`, or `Frozenset`, with a `Choice` as the `value_type`: + +```python +class IMySchema(model.Schema): + + myList = schema.List( + value_type=schema.Choice(...) + ) +``` + +The `Choice` field must be passed one of the following arguments: + +- `values` can be used to give a list of static values. +- `vocabulary` can be used to refer to an `IVocabulary` instance or (more commonly) a string giving the name of an `IVocabularyFactory` named utility. +- `source` can be used to refer to an `IContextSourceBinder` or `ISource` instance. + +In the remainder of this section, we will show the various techniques for defining vocabularies through several iterations of a new field added to the `Program` type, allowing the user to pick the organizer responsible for the program. + + +## Static vocabularies + +Our first attempt uses a static list of organizers. +We use the message factory to allow the labels to be translated. +The values stored in the `organizer` field will be a Unicode object representing the chosen label, or `None` if no value is selected: + +```python +from zope.schema.vocabulary import SimpleTerm +from zope.schema.vocabulary import SimpleVocabulary + +organizers = SimpleVocabulary( + [ + SimpleTerm(value='Bill', title=_('Bill')), + SimpleTerm(value='Bob', title=_('Bob')), + SimpleTerm(value='Jim', title=_('Jim')) + ] +) + +organizer = schema.Choice( + title=_('organizer"), + vocabulary=organizers, + required=False, +) +``` + +Since `required` is `False`, there will be a {guilabel}`no value` option in the drop-down list. + + +## Dynamic sources + +The static vocabulary is obviously a bit limited, since it is hard-coded in Python. + +We can make a one-off dynamic vocabulary using a context source binder. +This is a callable, usually a function or an object with a `__call__` method. +It provides the `IContextSourceBinder` interface and takes a `context` parameter. +The `context` argument is the context of the form, in other words, the folder on an add form, and the content object on an edit form. +The callable should return a vocabulary, which is most easily achieved by using the `SimpleVocabulary` class from `zope.schema`. + +Here is an example using a function to return all users in a particular group: + +```python +from Products.CMFCore.utils import getToolByName +from zope.interface import provider +from zope.schema.interfaces import IContextSourceBinder +from zope.schema.vocabulary import SimpleVocabulary + +@provider(IContextSourceBinder) +def possibleOrganizers(context): + acl_users = getToolByName(context, 'acl_users') + group = acl_users.getGroupById('organizers') + terms = [] + + if group is not None: + for member_id in group.getMemberIds(): + user = acl_users.getUserById(member_id) + if user is not None: + member_name = user.getProperty('fullname') or member_id + terms.append( + SimpleVocabulary.createTerm( + member_id, + str(member_id), + member_name + ) + ) + + return SimpleVocabulary(terms) +``` + +We use the `PluggableAuthService` API to get the group and its members. +A list of `terms` is created. +The list is passed to the constructor of a `SimpleVocabulary`. +The `SimpleVocabulary` object is returned. + +When working with vocabularies, you'll come across some terminology that is worth explaining: + +- A *term* is an entry in the vocabulary. + The term has a value. + Most terms are *tokenized* terms which also have a token, and some terms are *titled*, meaning they have a title that is different to the token. +- The *token* must be an ASCII string. + It is the value passed with the request when the form is submitted. + A token must uniquely identify a term. +- The *value* is the actual value stored on the object. + This is not passed to the browser or used in the form. + The value is often a Unicode object, but can be any type of object. +- The *title* is a Unicode object or translatable message (`zope.i18nmessageid`). + It is used in the form. + +The `SimpleVocabulary` class contains two class methods that can be used to create vocabularies from lists: + +`fromValues()` +: takes a simple list of values and returns a tokenized vocabulary where the values are the items in the list, and the tokens are created by calling `str()` on the values. + +`fromItems()` +: takes a list of `(token, value)` tuples and creates a tokenized vocabulary with the token and value specified. + +You can also instantiate a `SimpleVocabulary` yourself and pass a list of terms in the initializer. +The `createTerm()` class method can be used to create a term from a `value`, `token`, and `title`. +Only the value is required. + +Also to mention, `plone.app.vocabularies` has some helpers creating Unicode safe vocabularies. + +In the example above, we have chosen to create a `SimpleVocabulary` from terms with the user id used as value and token, and the user's full name as a title. + +To use this context source binder, we use the `source` argument to the `Choice` constructor: + +```python +organizer = schema.Choice( + title=_('organizer"), + source=possibleOrganizers, + required=False, +) +``` + + +## parameterized sources + +We can improve this example by moving the group name out of the function, allowing it to be set on a per-field basis. +To do so, we turn our `IContextSourceBinder` into a class that is initialized with the group name: + +```python +from zope.interface import implementer + +@implementer(IContextSourceBinder) +class GroupMembers(object): + """Context source binder to provide a vocabulary of users in a given + group. + """ + + def __init__(self, group_name): + self.group_name = group_name + + def __call__(self, context): + acl_users = getToolByName(context, 'acl_users') + group = acl_users.getGroupById(self.group_name) + terms = [] + + if group is not None: + for member_id in group.getMemberIds(): + user = acl_users.getUserById(member_id) + if user is not None: + member_name = user.getProperty('fullname') or member_id + terms.append( + SimpleVocabulary.createTerm( + member_id, + str(member_id), + member_name + ) + ) + + return SimpleVocabulary(terms) +``` + +Again, the source is set using the `source` argument to the `Choice` constructor: + +```python +organizer = schema.Choice( + title=_('organizer"), + source=GroupMembers('organizers'), + required=False, +) +``` + +When the schema is initialized on startup, a `GroupMembers` object is instantiated, storing the desired group name. +Each time the vocabulary is needed, this object will be called (in other words, the `__call__()` method is invoked) with the context as an argument, expected to return an appropriate vocabulary. + + +## Named vocabularies + +Context source binders are great for simple dynamic vocabularies. +They are also reusable, since you can import the source from a single location and use it in multiple instances. + +Sometimes, however, we want to provide an additional level of decoupling, by using *named* vocabularies. +These are similar to context source binders, but are components registered as named utilities, referenced in the schema by name only. +This allows local overrides of the vocabulary via the Component Architecture, and makes it easier to distribute vocabularies in third party packages. + +```{note} +Named vocabularies cannot be parameterized in the way as we did with the `GroupMembers` context source binder, since they are looked up by name only. +``` + +We can turn our first "members in the *organizers* group" vocabulary into a named vocabulary by creating a named utility providing `IVocabularyFactory`. +Create a vocabulary factory in {file}`vocabularies.py`: + +```python +from zope.schema.interfaces import IVocabularyFactory + +@provider(IVocabularyFactory) +def organizers_vocabulary_factory(context): + acl_users = getToolByName(context, 'acl_users') + group = acl_users.getGroupById('organizers') + terms = [] + + if group is not None: + for member_id in group.getMemberIds(): + user = acl_users.getUserById(member_id) + if user is not None: + member_name = user.getProperty('fullname') or member_id + terms.append( + SimpleVocabulary.createTerm( + member_id, + str(member_id), + member_name + ) + ) + + return SimpleVocabulary(terms) +``` + +Then add to your {file}`configure.zcml`. +By convention, the vocabulary name is prefixed with the package name, to ensure uniqueness. + +```xml + +``` + +We can make use of this vocabulary in any schema by passing its name to the `vocabulary` argument of the `Choice` field constructor: + +```python +organizer = schema.Choice( + title=_('organizer"), + vocabulary='example.conference.organizers", + required=False, +) +``` + + +## Using common vocabularies + +As you might expect, there are a number of standard vocabularies that come with Plone. +These are found in the [`plone.app.vocabularies`](https://pypi.org/project/plone.app.vocabularies/) package. +A recent and complete list can be found in the README of the package. + +For our example we could use `plone.app.vocabularies.Users`, which lists the users of the portal. + +The `organizer` field should now appear as shown. + +```python +organizer = schema.Choice( + title=_('organizer"), + vocabulary='plone.app.vocabularies.Users", + required=False, +) +``` + + +## The autocomplete selection widget + +The `organizer` field now has a query-based source. +The standard selection widget (a drop-down list) is not capable of rendering such a source. +Instead, we need to use a more powerful widget. +For a basic widget, see [`z3c.formwidget.query`](https://pypi.org/project/z3c.formwidget.query/). +But, in a Plone context, you will more likely want to use [`plone.formwidget.autocomplete`](https://pypi.org/project/plone.formwidget.autocomplete/), which extends `z3c.formwidget.query` to provide a friendlier user interface. + +The widget is provided with [`plone.app.dexterity`](https://pypi.org/project/plone.app.dexterity/), so we do not need to configure it ourselves. +We only need to tell Dexterity to use this widget instead of the default, using a form widget hint as shown earlier. +At the top of {file}`program.py`, we add the following import: + +```python +from plone.formwidget.autocomplete import AutocompleteFieldWidget +``` + +```{note} +If we were using a multi-valued field, such as a `List` with a `Choice` for `value_type`, we would use the `AutocompleteMultiFieldWidget` instead. +``` + +In the `IProgram` schema (which derives from `model.Schema`, and is therefore processed for form hints at startup), +we then add the following: + +```python +from plone.autoform import directives + +directives.widget(organizer=AutocompleteFieldWidget) +organizer = schema.Choice( + title=_('organizer'), + vocabulary='plone.app.vocabularies.Users', + required=False, +) +``` + +You should now see a dynamic auto-complete widget on the form, so long as you have JavaScript enabled. +Start typing a user name and see what happens. +The widget also has a fall-back for non-JavaScript capable browsers. diff --git a/plone.app.dexterity/advanced/webdav-and-other-file-representations.md b/plone.app.dexterity/advanced/webdav-and-other-file-representations.md new file mode 100644 index 000000000..b1902a81b --- /dev/null +++ b/plone.app.dexterity/advanced/webdav-and-other-file-representations.md @@ -0,0 +1,435 @@ +--- +myst: + html_meta: + "description": "How to add support for WebDAV, and accessing and modifying a content object using file-like operations in Plone" + "property=og:description": "How to add support for WebDAV, and accessing and modifying a content object using file-like operations in Plone" + "property=og:title": "How to add support for WebDAV, and accessing and modifying a content object using file-like operations in Plone" + "keywords": "Plone, content types, WebDAV, file representations" +--- + +# WebDAV and other file representations + +```{todo} +This chapter may have obsolete content. +It needs a content expert to review for accuracy for Plone 6. +``` + +This chapter describes how to add support for WebDAV, and accessing and modifying a content object using file-like operations. + +Zope supports WebDAV, a protocol that allows content objects to be viewed, modified, copied, renamed, moved, and deleted as if they were files on the filesystem. +WebDAV is also used to support saving to remote locations from various desktop programs. + +```{todo} +The following feature existed up until Plone 5.2. +There is an [open issue to restore this feature for Plone 6](https://github.com/plone/Products.CMFPlone/issues/3190). + +> In addition, WebDAV powers the [External Editor] product, which allows users to launch a desktop program from within Plone to edit a content object. +``` + +To configure a WebDAV server, you can add the following option to the `[instance]` section of your `buildout.cfg` and re-run buildout. + +```ini +webdav-address = 9800 +``` + +See the documentation for [`plone.recipe.zope2instance`](https://pypi.org/project/plone.recipe.zope2instance/) for details. +When Zope is started, you should now be able to mount it as a WebDAV server on the given port. + +Most operating systems support mounting WebDAV servers as folders. +Unfortunately, not all WebDAV implementations are very good. +Dexterity content should work with Windows Web Folders [^id2] and well-behaved clients. + +[^id2]: open Internet Explorer, go to {guilabel}`File | Open`, type in a WebDAV address, e.g. , and then select {guilabel}`Open as web folder` before hitting {guilabel}`OK`. + +On macOS, the Finder claims to support WebDAV, but the implementation is so flaky that it is just as likely to crash macOS as it is to let you browse files and folders. +Use a dedicated WebDAV client instead, such as [Cyberduck](https://cyberduck.io/). + + +## Default WebDAV behavior + +By default, Dexterity content can be downloaded and uploaded using a text format based on {RFC}`2822`, the same standard used to encode email messages. +Most fields are encoded in headers, whilst the field marked as "primary" will be contained in the body of the message. +If there is more than one primary field, a multi-part message is created. + +A field can be marked as "primary" using the `primary()` directive from [plone.supermodel](https://pypi.org/project/plone.supermodel/), as shown in the following example. + +```python +from plone.autoform import directives as form +from plone.supermodel import directives + +class ISession(model.Schema): + """A conference session. Sessions are managed inside Programs. + """ + + title = schema.TextLine( + title=_("Title"), + description=_("Session title"), + ) + + description = schema.Text( + title=_("Session summary"), + ) + + directives.primary("details") + details = RichText( + title=_("Session details"), + required=False + ) + + form.widget(presenter=AutocompleteFieldWidget) + presenter = RelationChoice( + title=_("Presenter"), + source=ObjPathSourceBinder(object_provides=IPresenter.__identifier__), + required=False, + ) + + form.write_permission(track="example.conference.ModifyTrack") + track = schema.Choice( + title=_("Track"), + source=possibleTracks, + required=False, + ) +``` + +This will actually apply the `IPrimaryField` marker interface from the [`plone.rfc822`](https://pypi.org/project/plone.rfc822/) package to the given fields. + +By default a WebDAV download of this content item will look like the following. + +```text +title: Test session +description: First session +presenter: 713399904 +track: Administrators +MIME-Version: 1.0 +Content-Type: text/html; charset="utf-8" +Portal-Type: example.conference.session + +

Details here

+``` + +Notice how most fields are encoded as header strings. +The `presenter` relation field stores a number, which is the integer ID of the target object. +Note that this ID is generated when the content object is created, and so is unlikely to be valid on a different site. +The `details` field, which we marked as primary, is encoded in the body of the message. + +It is also possible to upload such a file to create a new session. +To do that, the `content_type_registry` tool needs to be configured with a predicate that can detect the type of content from the uploaded file and instantiate the correct type of object. +Such predicates could be based on an extension or a filename pattern. +Below, we will see a different approach that uses a custom "file factory" for the containing `Program` type. + + +### Containers + +Container objects will be shown as *collections* (WebDAV-speak for folders) for WebDAV purposes. +This allows the WebDAV client to open the container and list its contents. +However, representing containers as collections makes it impossible to access the data contained in the various fields of the content object. + +To allow access to this information, a pseudo-file called `_data` will be exposed inside a Dexterity container. +This file can be read and written like any other, to access or modify the container's data. +It cannot be copied, moved, renamed, or deleted. +Those operations should be performed on the container itself. + + +## Customizing WebDAV behavior + +There are several ways in which you can influence the WebDAV behavior of your type. + +- If you are happy with the {RFC}`2822` format, you can provide your own `plone.rfc822.interfaces.IFieldMarshaler` adapters to provide alternate serializations and parsers for fields. + See the [`plone.rfc822`](http://pypi.python.org/pypi/plone.rfc822) documentation for details. +- If you want to use a different file representation, you can provide your own `IRawReadFile` and `IRawWriteFile` adapters. + For example, if you have a content object that stores binary data, you could return this data directly, with an appropriate MIME type, to allow it to be edited in a desktop program, such as an image editor if the MIME type is `image/jpeg`. + The file {file}`plone.dexterity.filerepresentation.py` contains two base classes, `ReadFileBase` and `WriteFileBase`, which you may be able to use to make it easier to implement these interfaces. +- If you want to control how content objects are created when a new file or directory is dropped into a particular type of container, you can provide your own `IFileFactory` or `IDirectoryFactory` adapters. + See [plone.dexterity.filerepresentation](https://github.com/plone/plone.dexterity/blob/master/plone/dexterity/filerepresentation.py) for the default implementations. + +As an example, let's register a custom `IFileFactory` adapter for the `IProgram` type. +This adapter will not rely on the `content_type_registry` tool to determine which type to construct, but will instead create a `Session` object, since that is the only type that is allowed inside a `Program` container. + +The code, in {file}`program.py`, is the following. + +```python +from zope.component import adapter +from zope.component import createObject +from zope.interface import implementer +from zope.event import notify +from zope.lifecycleevent import ObjectCreatedEvent +from zope.filerepresentation.interfaces import IFileFactory + +@implementer(IFileFactory) +@adapter(IProgram) +class ProgramFileFactory(object): + """Custom file factory for programs, which always creates a Session. + """ + + def __init__(self, context) + self.context = context + + def __call__(self, name, contentType, data): + session = createObject('example.conference.session', id=name) + notify(ObjectCreatedEvent(session)) + return session +``` + +We need to register the adapter in {file}`configure.zcml`. + +```xml + +``` + +This adapter overrides the `DefaultFileFactory` found in `plone.dexterity.filerepresentation`. +It creates an object of the designated type, fires an `IObjectModifiedEvent` and then returns the object, which will then be populated with data from the uploaded file. + +To test this, you could write a text file like the one shown above in a text editor and save it on your desktop, then drag it into the folder in your WebDAV client representing a `Program`. + +The following is an automated integration test for the same component. + +```python +def test_file_factory(self): + self.folder.invokeFactory("example.conference.program", "p1") + p1 = self.folder["p1"] + fileFactory = IFileFactory(p1) + newObject = fileFactory("new-session", "text/plain", "dummy") + self.assertTrue(ISession.providedBy(newObject)) +``` + + +## How it all works + +The rest of this section describes in some detail how the various WebDAV related components interact in Zope 2, CMF, and Dexterity. +This may be helpful if you are trying to customize or debug WebDAV behavior. + + +### Background + +Basic WebDAV support can be found in the `webdav` package. +This defines two base classes, `webdav.Resource.Resource` and `webdav.Collection.Collection`. +`Collection` extends `Resource`. +These are mixed into *item* and *container* content objects, respectively. + +The `webdav` package also defines the `NullResource` object. +A `NullResource` is a kind of placeholder, which supports the HTTP verbs `HEAD`, `PUT`, and `MKCOL`. + +Contents based on `ObjectManager` (including those in Dexterity) will return a `NullResource` if they cannot find the requested object and the request is a WebDAV request. + +The [`zope.filerepresentation`](https://pypi.org/project/zope.filerepresentation/) package defines a number of interfaces which are intended to help manage file representations of content objects. +Dexterity uses these interfaces to allow the exact file read and write operations to be overridden without subclassing. + + +### `HEAD` + +A `HEAD` request retrieves headers only. + +`Resource.HEAD()` sets `Content-Type` based on `self.content_type()`, `Content-Length` based on `self.get_size()`, `Last-Modified` based on `self._p_mtime`, and an `ETag` based on `self.http__etag()`, if available. + +`Collection.HEAD()` looks for `self.index_html.HEAD()` and returns its value if that exists. +Otherwise, it returns a "405 Method Not Allowed" response. +If there is no `index_html` object, it returns "404 Not Found". + + +### `GET` + +A `GET` request retrieves headers and body. + +Zope calls `manage_DAVget()` to retrieve the body. +The default implementation calls `manage_FTPget()`. + +In Dexterity, `manage_FTPget()` adapts `self` to `IRawReadFile` and uses its `mimeType` and `encoding` properties to set the `Content-Type` header, and its `size()` method to set `Content-Length`. + +If the `IRawReadFile` adapter is also an `IStreamIterator`, it will be returned for the publisher to consume directly. +This provides for efficient serving of large files, although it does require that the file can be read in its entirety with the ZODB connection closed. +Dexterity solves this problem by writing the file content to a temporary file on the server. + +If the `IRawReadFile` adapter is not a stream iterator, its contents are returned as a string, by calling its `read()` method. +Note that this loads the entire file contents into memory on the server. + +The default `IRawReadFile` implementation for Dexterity content returns an {RFC}`2822`-style message document. +Most fields on the object and any enabled behaviors will be turned into UTF-8 encoded headers. +The primary field, if any, will be returned in the body, also most likely encoded as a UTF-8 encoded string. +Binary data may be base64-encoded instead. + +A type which wishes to override this behavior can provide its own adapter. +For example, an image type could return the raw image data. + + +### `PUT` + +A `PUT` request reads the body of a request and uses it to update a resource that already exists, or to create a new object. + +By default `Resource.PUT()` fails with "405 Method Not Allowed". +That is, it is not by default possible to `PUT` to a resource that already exists. +The same is true of `Collection.PUT()`. + +In Dexterity, the `PUT()` method is overridden to adapt `self` to `zope.filerepresentation.IRawWriteFile`, and call its `write()` method one or more times, writing the contents of the request body, before calling `close()`. +The `mimeType` and `encoding` properties will also be set based on the value of the `Content-Type` header, if available. + +The default implementation of `IRawWriteFile` for Dexterity objects assumes the input is an RFC 2822 style message document. +It will read header values and use them to set fields on the object or in behaviors, and similarly read the body and update the corresponding primary field. + +`NullResource.PUT()` is responsible for creating a new content object and initializing it (recall that a `NullResource` may be returned if a WebDAV request attempts to traverse to an object which does not exist). +It sniffs the content type and body from the request, and then looks for the `PUT_factory()` method on the parent folder. + +In Dexterity, `PUT_factory()` is implemented to look up an `IFileFactory` adapter on `self`, and use it to create the empty file. +The default implementation will use the `content_type_registry` tool to determine a type name for the request (for example, based on its extension or MIME type), and then construct an instance of that type. + +Once an instance has been constructed, the object will be initialized by calling its `PUT()` method, as above. + +Note that when content is created via WebDAV, an `IObjectCreatedEvent` will be fired from the `IFileFactory` adapter, just after the object has been constructed. +At this point, none of its values will be set. +Subsequently, at the end of the `PUT()` method, an `IObjectModifiedEvent` will be fired. +This differs from the event sequence of an object created through the web. +Here, only an `IObjectCreatedEvent` is fired, and only *after* the object has been fully initialized. + + +### `DELETE` + +A `DELETE` request instructs the WebDAV server to delete a resource. + +`Resource.DELETE()` calls `manage_delObjects()` on the parent folder to delete an object. + +`Collection.DELETE()` does the same, but checks for write locks of all children of the collection, recursively, before allowing the delete. + + +### `PROPFIND` + +A `PROPFIND` request returns all or a set of WebDAV properties. +WebDAV properties are metadata used to describe an object, such as the last modified time or the author. + +`Resource.PROPFIND()` parses the request and then looks for a `propertysheets` attribute on `self`. + +If an `allprop` request is received, it calls `dav__allprop()`, if available, on each property sheet. +This method returns a list of name/value pairs in the correct WebDAV XML encoding, plus a status. + +If a `propnames` request is received, it calls `dav__propnames()`, if available, on each property sheet. +This method returns a list of property names in the correct WebDAV XML encoding, plus a status. + +If a `propstat` request is received, it calls `dav__propstats()`, if available, on each property sheet, for each requested property. +This method returns a property name/value pair in the correct WebDAV XML encoding, plus a status. + +The `PropertyManager` mixin class defines the `propertysheets` variable to be an instance of `DefaultPropertySheets`. +This in turn has two property sheets: + +- `default`, a `DefaultProperties` instance. +- `webdav`, a `DAVProperties` instance. + +The `DefaultProperties` instance contains the main property sheet. +This typically has a `title` property, for example. + +`DAVProperties` will provides various core WebDAV properties. +It defines a number of read-only properties: `creationdate`, `displayname`, +`resourcetype`, `getcontenttype`, `getcontentlength`, `source`, +`supportedlock`, and `lockdiscovery`. +These in turn are delegated to methods prefixed with `dav__`. +For example, reading the `creationdate` property calls `dav__creationdate()` on the +property sheet instance. +These methods in turn return values based on the property manager instance, in other words, the content object, as explained below. + +`creationdate` +: returns a fixed date (January 1st, 1970). + +`displayname` +: Returns the value of the `title_or_id()` method. + +`resourcetype` +: Returns an empty string or ``. + +`getlastmodified` +: Returns the ZODB modification time. + +`getcontenttype` +: Delegates to the `content_type()` method, falling back on the `default_content_type()` method. + In Dexterity, `content_type()` is implemented to look up the `IRawReadFile` adapter on the context and return the value of its `mimeType` property. + +`getcontentlength` +: Delegates to the `get_size()` method (which is also used for the `size` column in Plone folder listings). + In Dexterity, this looks up a `zope.size.interfaces.ISized` adapter on the object and calls `sizeForSorting()`. + If this returns a unit of `'bytes'`, the value portion is used. + Otherwise, a size of `0` is returned. + +`source` +: Returns a link to `/document_src`, if that attribute exists. + +`supportedlock` +: Indicates whether `IWriteLock` is supported by the content item. + +`lockdiscovery` +: Returns information about any active locks. + +Other properties in this and any other property sheets are returned as stored when requested. + +If the `PROPFIND` request specifies a depth of `1` or `infinity` (in other words, the client wants properties for items in a collection), the process is repeated for all items returned by the `listDAVObjects()` methods, which by default returns all contained items via the `objectValues()` method. + + +### `PROPPATCH` + +A `PROPPATCH` request is used to update the properties on an existing object. + +`Resource.PROPPATCH()` deals with the same types of properties from property sheets as `PROPFIND()`. +It uses the `PropertySheet` API to add or update properties as appropriate. + + +### `MKCOL` + +A `MKCOL` request is used to create a new collection resource, in other words, create a new folder. + +`Resource.MKCOL()` raises "405 Method Not Allowed", because the resource already exists +(remember that in WebDAV, the `MKCOL` request, like a `PUT` for a new resource, is sent with a location that specifies the desired new resource location, not the location of the parent object). + +`NullResource.MKCOL()` handles the valid case where a `MKCOL` request has been sent to a new resource. +After checking that the resource does not already exist, that the parent is indeed a collection (folderish item), and that the parent is not locked, it calls the `MKCOL_handler()` method on the parent folder. + +In Dexterity, the `MKCOL()_handler` is overridden to adapt `self` to an +`IDirectoryFactory` from `zope.filerepresentation` and use this to create a directory. +The default implementation calls `manage_addFolder()` on the parent. +This will create an instance of the `Folder` type. + + +### `COPY` + +A `COPY` request is used to copy a resource. + +`Resource.COPY()` implements this operation using the standard Zope content object copy semantics. + + +### `MOVE` + +A `MOVE` request is used to relocate or rename a resource. + +`Resource.MOVE()` implements this operation using the standard Zope content-object move semantics. + + +### `LOCK` + +A `LOCK` request is used to lock a content object. + +All relevant WebDAV methods in the `webdav` package are lock aware. +That is, they check for locks before attempting any operation that would violate a lock. + +Also note that [`plone.locking`](https://pypi.org/project/plone.locking/) uses the lock implementation from the `webdav` package by default. + +`Resource.LOCK()` implements locking and lock refresh support. + +`NullResource.LOCK()` implements locking on a `NullResource`. +In effect, this means locking the name of the non-existent resource. +When a `NullResource` is locked, it is temporarily turned into a `LockNullResource` object, which is a persistent object set onto the parent (remember that a `NullResource` is a transient object returned when a child object cannot be found in a WebDAV request). + + +### `UNLOCK` + +An `UNLOCK` request is used to unlock a locked object. + +`Resource.UNLOCK()` handles unlock requests. + +`LockNullResource.UNLOCK()` handles unlocking of a `LockNullResource`. +This deletes the `LockNullResource` object from the parent container. + + +### Fields on container objects + +When browsing content via WebDAV, a container object (folderish item) will appear as a folder. +Most likely, this object will also have content in the form of schema fields. +To make this accessible, Dexterity containers expose a pseudo-file with the name `_data`, by injecting this into the return value of `listDAVObjects()` and adding a special traversal hook to allow its contents to be retrieved. + +This file supports `HEAD`, `GET`, `PUT`, `LOCK`, `UNLOCK`, `PROPFIND`, and `PROPPATCH` requests. +An error will be raised if the user attempts to rename, copy, move, or delete it. +These operate on the container object, obviously. +For example, when the data object is updated via a PUT request, the `PUT()` method on the container is called, by default delegating to an `IRawWriteFile` adapter on the container. diff --git a/plone.app.dexterity/advanced/workflow.md b/plone.app.dexterity/advanced/workflow.md new file mode 100644 index 000000000..9163ea7b1 --- /dev/null +++ b/plone.app.dexterity/advanced/workflow.md @@ -0,0 +1,264 @@ +--- +myst: + html_meta: + "description": "How to control security of content types with workflow in Plone" + "property=og:description": "How to control security of content types with workflow in Plone" + "property=og:title": "How to control security of content types with workflow in Plone" + "keywords": "Plone, security, content types, workflow" +--- + +# Workflow + +This chapter describes how to control security with workflow. + +Workflow is used in Plone for three distinct, but overlapping purposes. + +- To keep track of metadata, chiefly an object's *state*. +- To create content review cycles and model other types of processes. +- To manage object security. + +When writing content types, we will often create custom workflows to go with them. +In this section, we will explain at a high level how Plone's workflow system works, and then show an example of a simple workflow to go with our example types. +An exhaustive manual on using workflows is beyond the scope of this manual, but hopefully this will cover the basics. + + +## A DCWorkflow refresher + +What follows is a fairly detailed description of [`DCWorkflow`](https://pypi.org/project/Products.DCWorkflow/). +You may find some of this a little detailed on first reading, so feel free to skip to the specifics later on. +However, it is useful to be familiar with the high level concepts. +You're unlikely to need multi-workflow chains in your first few attempts at workflow, for instance, but it's useful to know what it is if you come across the term. + +Plone's workflow system is known as DCWorkflow. +It is a *states-and-transitions* system, which means that your workflow starts in a particular *state* (the *initial state*) and then moves to other states via *transitions* (also called *actions* in CMF). + +When an object enters a particular state (including the initial state), the workflow is given a chance to update **permissions** on the object. +A workflow manages a number of permissions—typically the "core" CMF permissions +including {guilabel}`View`, {guilabel}`Modify portal content`, and so on—and will set those on the object at each state change. +Note that this is event-driven, rather than a real-time security check. +Only by changing the state is the security information updated. +This is why you need to click {guilabel}`Update security settings` at the bottom of the `portal_workflow` screen in the ZMI when you change your workflows' security settings and +want to update existing objects. + +A state can also assign *local roles* to *groups*. +This is akin to assigning roles to groups on Plone's {guilabel}`Sharing` tab, but the mapping of roles to groups happens on each state change, much like the mapping of roles to permissions. +Thus, you can say that in the `pending_secondary` state, members of the {guilabel}`Secondary reviewers` group have the {guilabel}`Reviewer` local role. +This is powerful stuff when combined with the more usual role-to-permission mapping, although it is not very commonly used. + +State changes result in a number of *variables* being recorded, such as the *actor* (the user that invoked the transition), the *action* (the name of the transition), the date and time, and so on. +The list of variables is dynamic, so each workflow can define any number of variables linked to [TALES](https://zope.readthedocs.io/en/latest/zopebook/AppendixC.html#tales-overview) expressions that are invoked to calculate the current value at the point of transition. +The workflow also keeps track of the current state of each object. +The state is exposed as a special type of workflow variable called the *state variable*. +Most workflows in Plone uses the name `review_state` as the state variable. + +Workflow variables are recorded for each state change in the *workflow history*. +This allows you to see when a transition occurred, who effected it, and what state the object was in before or after. +In fact, the "current state" of the workflow is internally looked up as the most recent entry in the workflow history. + +Workflow variables are also the basis for *worklists*. +These are basically pre-defined catalog queries run against the current set of workflow variables. +Plone's review portlet shows all current worklists from all installed workflows. +This can be a bit slow, but it does mean that you can use a single portlet to display an amalgamated list of all items on all worklists that apply to the current user. +Most Plone workflows have a single worklist that matches on the `review_state` variable, for example, showing all items in the `pending` state. + +If states are the static entities in the workflow system, *transitions* (actions) provide the dynamic parts. +Each state defines zero or more possible exit transitions, and each transition defines exactly one target state, though it is possible to mark a transition as "stay in current state". +This can be useful if you want to do something in reaction to a transition and record that the transition happened in the workflow history, but not change the state (or security) of the object. + +Transitions are controlled by one or more *guards*. +These can be permissions (the preferred approach), roles (mostly useful for the {guilabel}`Owner` role; in other cases it is normally better to use permissions) or TALES expressions. +A transition is available if all its guard conditions are true. +A transition with no guard conditions is available to everyone, including the anonymous user. + +Transitions are user-triggered by default, but may be **automatic**. +An automatic transition triggers immediately following another transition, provided its guard conditions pass. +It will not necessarily trigger as soon as the guard condition becomes true, as that would involve continually re-evaluating guards for all active workflows on all objects. + +When a transition is triggered, the `IBeforeTransitionEvent` and `IAfterTransitionEvent` **events** +are triggered. +These are low-level events from `Products.DCWorkflow` that can tell you a lot about the previous and current states. +There is a higher level `IActionSucceededEvent` in `Products.CMFCore` that is more commonly used to react after a workflow action has completed. + +In addition to the events, you can configure workflow **scripts**. +These are either created through-the-web or (more commonly) as External Methods [^id2], and may be set to execute before a transition is complete, that is, before the object enters the target state, or just after it has been completed when the object is in the new state. +Note that if you are using event handlers, you'll need to check the event object to find out which transition was invoked, since the events are fired on all transitions. +The per-transition scripts are only called for the specific transitions for which they were configured. + +[^id2]: An *External Method* is a Python script evaluated in Zope context. + See [Logic Objects](https://zope.readthedocs.io/en/latest/zopebook/BasicObject.html#logic-objects-script-python-objects-and-external-methods) in the Zope 2 Book. + + +### Multi-chain workflows + +Workflows are mapped to types via the `portal_workflow` tool. +There is a default workflow, indicated by the string `(Default)`. +Some types have no workflow, which means that they hold no state information and typically inherit permissions from their parent. +It is also possible for types to have *multiple workflows*. +You can list multiple workflows by separating their names by commas. +This is called a *workflow chain*. + +Note that in Plone, the workflow chain of an object is looked up by multi-adapting the object and the workflow to the `IWorkflowChain` interface. +The adapter factory should return a tuple of string workflow names (`IWorkflowChain` is a specialization of `IReadSequence`, that is, a tuple). +The default obviously looks at the mappings in the `portal_workflow` tool, but it is possible to override the mapping, for example, by using a custom adapter registered for some marker interface, which in turn could be provided by a type-specific behavior. + +Multiple workflows applied in a single chain co-exist in time. +Typically, you need each workflow in the chain to have a different state variable name. +The standard `portal_workflow` API (in particular, `doActionFor()`, which is used to change the state of an object) also assumes the transition IDs are unique. +If you have two workflows in the chain and both currently have a `submit` action available, only the first workflow will be transitioned if you do `portal_workflow.doActionFor(context, ‘submit')`. +Plone will show all available transitions from all workflows in the current object's chain in the `State` drop-down, so you do not need to create any custom user interface for this. +However, Plone always assumes the state variable is called `review_state` (which is also the variable indexed in `portal_catalog`). +Therefore, the state of a secondary workflow won't show up unless you build some custom UI. + +In terms of security, remember that the role-to-permission (and group-to-local-role) mappings are event-driven and are set after each transition. +If you have two concurrent workflows that manage the same permissions, the settings from the last transition invoked will apply. +If they manage different permissions (or there is a partial overlap), then only the permissions managed by the most-recently-invoked workflow will change, leaving the settings for other permissions untouched. + +Multiple workflows can be very useful in case you have concurrent processes. +For example, an object may be published, but require translation. +You can track the review state in the main workflow and the translation state in another. +If you index the state variable for the second workflow in the catalog (the state variable is always available on the indexable object wrapper, so you only need to add an index with the appropriate name to `portal_catalog`), you can search for all objects pending translation, for example, using a *Collection*. + + +## Creating a new workflow + +With the theory out of the way, let's show how to create a new workflow. + +Workflows are managed in the `portal_workflow` tool. +You can use the ZMI to create new workflows and assign them to types. +However, it is usually preferable to create an installable workflow configuration using GenericSetup. +By default, each workflow, as well as the workflow assignments, are imported and exported using an XML syntax. +This syntax is comprehensive, but rather verbose if you are writing it manually. + +For the purposes of this manual, we will show an alternative configuration syntax based on spreadsheets (in CSV format). +This is provided by the [`collective.wtf`](https://pypi.org/project/collective.wtf/) package. +You can read more about the details of the syntax in its documentation. +Here, we will only show how to use it to create a simple workflow for the `Session` type, allowing members to submit sessions for review. + +To use `collective.wtf`, we need to depend on it. +In {file}`setup.py`, we have the following. + +```python +install_requires=[ + 'collective.wtf', +], +``` + +```{note} +As before, the `` line in {file}`configure.zcml` takes care of configuring the package for us. +``` + +A workflow definition using `collective.wtf` consists of a CSV file in the `profiles/default/workflow_csv` directory, which we will create, and a {file}`workflows.xml` file in `profiles/default` which maps types to workflows. + +The workflow mapping in {file}`profiles/default/workflows.xml` is shown below. + +```xml + + + + + + + + +``` + +The CSV file itself is found in {file}`profiles/default/workflow_csv/example.conference.session_workflow.csv`. +It contains the following, which was exported to CSV from an OpenOffice spreadsheet. +You can find the original spreadsheet with the [`example.conference` source code](https://github.com/collective/example.conference/tree/master/example/conference/profiles/default/workflow_csv). +This applies some useful formatting, which is obviously lost in the CSV version. + +```{note} +For your own workflows, you may want to use [this template](https://github.com/collective/example.conference/blob/master/example/conference/profiles/default/workflow_csv/example.conference.session_workflow.ods) as a starting point. +``` + +```text +"[Workflow]" +"Id:","example.conference.session_workflow" +"Title:","Conference session workflow" +"Description:","Allows members to submit session proposals for review" +"Initial state:","draft" + +"[State]" +"Id:","draft" +"Title:","Draft" +"Description:","The proposal is being drafted." +"Transitions","submit" +"Permissions","Acquire","Anonymous","Authenticated","Member","Manager","Owner","Editor","Reader","Contributor","Reviewer" +"View","N",,,,"X","X","X","X",, +"Access contents information","N",,,,"X","X","X","X",, +"Modify portal content","N",,,,"X","X","X",,, + + +"[State]" +"Id:","pending" +"Title:","Pending" +"Description:","The proposal is pending review" +"Worklist:","Pending review" +"Worklist label:","Conference sessions pending review" +"Worklist guard permission:","Review portal content" +"Transitions:","reject, publish" +"Permissions","Acquire","Anonymous","Authenticated","Member","Manager","Owner","Editor","Reader","Contributor","Reviewer" +"View","N",,,,"X","X","X","X",,"X" +"Access contents information","N",,,,"X","X","X","X",,"X" +"Modify portal content","N",,,,"X","X","X",,,"X" + +"[State]" +"Id:","published" +"Title:","Published" +"Description:","The proposal has been accepted" +"Transitions:","reject" +"Permissions","Acquire","Anonymous","Authenticated","Member","Manager","Owner","Editor","Reader","Contributor","Reviewer" +"View","Y","X",,,,,,,, +"Access contents information","Y","X",,,,,,,, +"Modify portal content","Y",,,,"X","X","X",,, + +"[Transition]" +"Id:","submit" +"Title:","Submit" +"Description:","Submit the session for review" +"Target state:","pending" +"Guard permission:","Request review" + +"[Transition]" +"Id:","reject" +"Title:","Reject" +"Description:","Reject the session from the program" +"Target state:","draft" +"Guard permission:","Review portal content" + +"[Transition]" +"Id:","publish" +"Title:","Publish" +"Description:","Accept and publish the session proposal" +"Target state:","published" +"Guard permission:","Review portal content" +``` + +Here, you can see several states and transitions. +Each state contains a role/permission map, and a list of the possible exit transitions. +Each transition contains a target state and other meta-data, such as a title and a description, as well as guard permissions. + +```{note} +Like most other GenericSetup import steps, the workflow uses the Zope 2 permission title when referring to permissions. +``` + +When the package is (re-)installed, this workflow should be available under `portal_workflow` and mapped to the `Session` type. + +```{note} +If you have existing instances, don't forget to go to `portal_workflow` in the ZMI and click {guilabel}`Update security settings` at the bottom of the page. +This ensures that existing objects reflect the most recent security settings in the workflow. +``` + +## A note about add permissions + +This workflow assumes that regular members can add *Session* proposals to *Programs*, which are then reviewed. +Previously, we granted the `example.conference: Add session` permission to the `Member` role. +This is necessary, but not sufficient to allow members to add sessions to programs. +The user will also need the generic `Add portal content` permission in the `Program` folder. + +There are two ways to achieve this: + +- Build a workflow for the `Program` type that manages this permission. +- Use the {guilabel}`Sharing` tab to grant {guilabel}`Can add` to the {guilabel}`Authenticated Users` group. + This grants the {guilabel}`Contributor` local role to members. + By default, this role is granted the {guilabel}`Add portal content` permission. diff --git a/plone.app.dexterity/behaviors/behavior-basics.md b/plone.app.dexterity/behaviors/behavior-basics.md new file mode 100644 index 000000000..24f4310fb --- /dev/null +++ b/plone.app.dexterity/behaviors/behavior-basics.md @@ -0,0 +1,35 @@ +--- +myst: + html_meta: + "description": "The fundamental concepts behind behaviors for content types in Plone" + "property=og:description": "The fundamental concepts behind behaviors for content types in Plone" + "property=og:title": "The fundamental concepts behind behaviors for content types in Plone" + "keywords": "Plone, content types, behavior, basics" +--- + +# Behavior basics + +This chapter describes the fundamental concepts behind behaviors. + +Before we dive into the practical examples, we need to explain a few of the concepts that underpin behaviors. + +At the most basic level, a behavior is like a "conditional" adapter. +For a Dexterity content type, the default condition is, "is this behavior listed in the `behaviors` property in the FTI?" +But the condition itself is an adapter; in rare cases this can be overruled. +When a behavior is enabled for a particular object, it will be possible to adapt that object to the behavior's interface. +If the behavior is disabled, adaptation will fail. + +A behavior consists at the very least of an interface and some metadata, namely a title and a description. +In most cases, there is also a *factory*, akin to an adapter factory, which will be invoked to get an appropriate adapter when requested. +This is usually just a class that looks like any other adapter factory, although it will tend to be applicable to `Interface`, `IContentish`, or a similarly broad context. + +Behaviors may specify a *marker interface*, which will be directly provided by instances for which the behavior is enabled. +This is useful if you want to conditionally enable event handlers or view components, which are registered for this marker interface. +Some behaviors have no factory. +In this case, the behavior interface and the marker interface must be one and the same. +If a factory is given, a marker interface different from the behavior interface must be declared. + +Behaviors are registered globally, using the `` ZCML directive. +This results in, among other things, a named utility providing `plone.behavior.interfaces.IBehavior` being registered. +This utility contains various information about the behavior, such as its name, title, interface, and (optional) marker interface. +The utility name is the full dotted name to the behavior interface. diff --git a/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md b/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md new file mode 100644 index 000000000..74e61704a --- /dev/null +++ b/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md @@ -0,0 +1,138 @@ +--- +myst: + html_meta: + "description": "How to create a basic behavior that provides form fields for content types in Plone" + "property=og:description": "How to create a basic behavior that provides form fields for content types in Plone" + "property=og:title": "How to create a basic behavior that provides form fields for content types in Plone" + "keywords": "Plone, behaviors, content types, create, register" +--- + +# Creating and registering behaviors + +This chapter describes how to create a basic behavior that provides form fields. + +The following example is based on the [`collective.gtags`](https://pypi.org/project/collective.gtags/) product. +It comes with a behavior that adds a `tags` field to the `Categorization` fieldset, storing the actual tags in the Dublin Core `Subject` field. + +`collective.gtags` is a standard package, with a `configure.zcml`, a GenericSetup profile, and a number of modules. +We won't describe those here, though, since we are only interested in the behavior. + +First, there are a few dependencies in {file}`setup.py`. + +```python +install_requires=[ + # ..., + "plone.behavior", + "zope.schema", + "zope.interface", + "zope.component", +], +``` + +Next, we have {file}`behaviors.zcml`, which is included from {file}`configure.zcml`, and contains all the necessary configuration to set up the behaviors. + +```xml + + + + + + + +``` + +We first include the `plone.behavior meta.zcml` file, so that we get access to the `` ZCML directive. + +The behavior itself is registered with the `` directive. +We set a `title` and a `description`, and then specify the `behavior interface` with the `provides` attribute. +This attribute is required, and is used to construct the unique name for the behavior. +In this case, the behavior name is `collective.gtags.behaviors.ITags`, the full dotted name to the behavior interface. +When the behavior is enabled for a type, it will be possible to adapt instances of that type to `ITags`. +That adaptation will invoke the factory specified by the `factory` attribute. + +The following is the {file}`behaviors.py` module. + +```python +"""Behaviors to assign tags (to ideas). + +Includes a form field and a behavior adapter that stores the data in the +standard Subject field. +""" + +from plone.dexterity.interfaces import DexterityContent +# if your package was made with mr.bob, add your MessageFactory as shown: +from collective.mypackage import _ +from plone.autoform.interfaces import IFormFieldProvider +from plone.supermodel import directives +from plone.supermodel import model +from zope.component import adapter +from zope.interface import implementer +from zope.interface import provider + + +@provider(IFormFieldProvider) +class ITags(model.Schema): + """Add tags to content + """ + + directives.fieldset( + "categorization", + label=_("Categorization"), + fields=("tags",), + ) + + tags = Tags( + title=_("Tags"), + description=_("Applicable tags"), + required=False, + allow_uncommon=True, + ) + + +@implementer(ITags) +@adapter(IDexterityContent) +class Tags(object): + """Store tags in the Dublin Core metadata Subject field. This makes + tags easy to search for. + """ + + def __init__(self, context): + self.context = context + + # the properties below are not necessary the first time when you just want to see your added field(s) + @property + def tags(self): + return set(self.context.Subject()) + @tags.setter + def tags(self, value): + if value is None: + value = () + self.context.setSubject(tuple(value)) +``` + +We first define the `ITags` interface, which is also the behavior interface. +Here we define a single attribute, `tags`, but we could also have added methods and additional fields if required. +Naturally, these need to be implemented by the behavior adapter. + +Since we want this behavior to provide form fields, we derive the behavior interface from `model.Schema` and set form hints using `plone.supermodel.directives`. +We also mark the `ITags` interface with `IFormFieldProvider` to signal that it should be processed for form fields by the standard forms. +See the {doc}`Dexterity Developer Manual <../index>` for more information about setting form hints in schema interfaces. + +If your behavior does not provide form fields, you can just derive from `zope.interface.Interface` and omit the `alsoProvides()` line. + +Next, we write the class that implements the behavior adapter and acts as the adapter factory. +Notice how it implements the behavior interface (`ITags`), and adapts a broad interface `(IDexterityContent`). +The behavior cannot be enabled on types not supporting this interface. +In many cases, you will omit the `adapter()` line, provided your behavior is generic enough to work on any context. + +The adapter is otherwise identical to any other adapter. +It implements the interface, here by storing values in the `Subject` field. diff --git a/plone.app.dexterity/behaviors/index.md b/plone.app.dexterity/behaviors/index.md new file mode 100644 index 000000000..e5224e38c --- /dev/null +++ b/plone.app.dexterity/behaviors/index.md @@ -0,0 +1,23 @@ +--- +myst: + html_meta: + "description": "How to create reusable behaviors for Dexterity content types in Plone" + "property=og:description": "How to create reusable behaviors for Dexterity content types in Plone" + "property=og:title": "How to create reusable behaviors for Dexterity content types in Plone" + "keywords": "Plone, reusable, behaviors, content types" +--- + +# Behaviors + +This section describes how to create reusable behaviors for Dexterity content types. + +```{toctree} +:maxdepth: 2 + +intro +behavior-basics +creating-and-registering-behaviors +providing-marker-interfaces +schema-only-behaviors +testing-behaviors +``` diff --git a/plone.app.dexterity/behaviors/intro.md b/plone.app.dexterity/behaviors/intro.md new file mode 100644 index 000000000..c236c1670 --- /dev/null +++ b/plone.app.dexterity/behaviors/intro.md @@ -0,0 +1,39 @@ +--- +myst: + html_meta: + "description": "How to write your own behaviors for content types in Plone" + "property=og:description": "How to write your own behaviors for content types in Plone" + "property=og:title": "How to write your own behaviors for content types in Plone" + "keywords": "Plone, behaviors, content types, introduction" +--- + +# Introduction + +This manual should teach you everything you need to know to write your own behaviors, but not how to integrate them into another framework. + +Behaviors are reusable bundles of functionality that can be enabled or disabled on a per-content type basis. +Examples might include: + +- A set of form fields (on standard add and edit forms) +- Enabling particular event handler +- Enabling one or more views, viewlets, or other UI components +- Anything else which may be expressed in code via an adapter or marker interface. + +You would typically not write a behavior as a one-off. +Behaviors are normally used when: + +- You want to share fields and functionality across multiple types easily. + Behaviors allow you to write a schema and associated components—including adapters, event handlers, views, and viewlets—once and reuse them. +- A more experienced developer makes functionality available for reuse by less experienced integrators. + For example, a behavior can be packaged up and released as an add-on product. + Integrators can then install that product, and use the behavior in their own types, either in code or through-the-web. + +This manual is aimed at developers who want to write new behaviors. +This is a slightly more advanced topic than the writing of custom content types. +It assumes you are familiar with buildout, know how to create a custom package, understand interfaces, and have a basic understanding of Zope's component architecture. + +Behaviors are not tied to Dexterity, but Dexterity provides behavior support for its types via the *behaviors* FTI property. +In fact, if you've used Dexterity before, you've probably used some behaviors. +Take a look at the {doc}`Dexterity Developer Manual <../index>` for more information about how to enable behaviors on a type, and for a list of standard behaviors. + +To learn more about how behaviors are implemented in detail, see the [`plone.behavior`](https://pypi.org/project/plone.behavior/) package. diff --git a/plone.app.dexterity/behaviors/providing-marker-interfaces.md b/plone.app.dexterity/behaviors/providing-marker-interfaces.md new file mode 100644 index 000000000..768f10f9f --- /dev/null +++ b/plone.app.dexterity/behaviors/providing-marker-interfaces.md @@ -0,0 +1,237 @@ +--- +myst: + html_meta: + "description": "How to use behaviors to set marker interfaces on instances of a given content type in Plone" + "property=og:description": "How to use behaviors to set marker interfaces on instances of a given content type in Plone" + "property=og:title": "How to use behaviors to set marker interfaces on instances of a given content type in Plone" + "keywords": "Plone, behaviors, content types, marker interfaces" +--- + +# Providing marker interfaces + +This chapter describes how to use behaviors to set marker interfaces on instances of a given content type. + +Sometimes it is useful for objects that provide a particular behavior to also provide a specific marker interface. +For example, you can register a viewlet for a particular marker, and use a behavior to enable that marker on all instances of a particular content type. +The viewlet will then only show up when the behavior is enabled. +The same principle can be applied to event handlers, views, and other components. + +```{note} +There is usually no need to use markers to enable a custom adapter since a standard behavior is already a conditional adapter. +However, in certain cases, you may want to provide one or more adapters to an interface that is not the behavior interface, such as to use a particular extension point provided by another component. +In this case, it may be easier to set a marker interface and provide an adapter from this marker. +``` + +`plone.behavior`'s marker support can be used in two ways. + +- As the behavior interface itself. + In this case, there is no behavior adapter factory. + The behavior interface and the marker interface are one and the same. +- As a supplement to a standard behavior adapter. + In this case, a factory is provided, and the behavior interface (which the behavior adapter factory implements) is different to the marker interface. + + +## Primary marker behaviors + +In the first case, where the behavior interface and the marker interface are the same, you can simply use the `` directive without a `factory`. + +```xml + +``` + +One could imagine a viewlet based on [`plone.pony`](https://pypi.org/project/plone.pony/) registered for the `IWantAPony` marker interface. +If the behavior is enabled for a particular object, `IWantAPony.providedBy(object)` would be true. + + +## Supplementary marker behaviors + +In the second case, we want to provide a behavior interface with a behavior adapter factory as usual, such as with some form fields and a custom storage or a few methods implemented in an adapter, but we also need a custom marker. +Here we use both the `provides` and `marker` attributes to `` to reference the two interfaces, as well as a `factory`. + +To a more interesting example, here is a behavior from a project that lets content authors with particular permissions (`iz.EditOfficialReviewers` and `iz.EditUnofficialReviewers`), nominate the "official" and any "unofficial" reviewers for a given content item. +The behavior provides the necessary form fields to support this. +It also sets a marker interface that enables the following. + +- an `ILocalRoleProvider` adapter to automatically grant local roles to the chosen reviewers +- a custom indexer that lists the reviewers + +The ZCML registration would be the following. + +```xml + +``` + +Notice the use of the `AnnotationStorage` factory. +This is a reusable factory that can be used to create behaviors from schema interfaces that store their values in annotations. +We'll describe this in more detail later. +We also could have provided our own factory in this example. + +The {file}`reviewers.py` module contains the following. + +```python +"""Behavior to enable certain users to nominate reviewers + +Includes form fields, an indexer to make it easy to find the items with +specific reviewers, and a local role provider to grant the Reviewer and +OfficialReviewer roles appropriately. +""" + +from Products.ZCatalog.interfaces import IZCatalog +from borg.localrole.interfaces import ILocalRoleProvider +from iz.behaviors import MessageFactory as _ +from plone.autoform import directives +from plone.autoform.interfaces import IFormFieldProvider +from plone.formwidget.autocomplete.widget import AutocompleteMultiFieldWidget +from plone.indexer.interfaces import IIndexer +from plone.supermodel import model +from zope import schema +from zope.component import adapter +from zope.interface import Interface +from zope.interface import provider + + +@provider(IFormFieldProvider) +class IReviewers(model.Schema): + """Support for specifying official and unofficial reviewers + """ + + directives.fieldset( + "ownership", + label=_("Ownership"), + fields=( + "official_reviewers", + "unofficial_reviewers" + ), + ) + + directives.widget(official_reviewers=AutocompleteMultiFieldWidget) + directives.write_permission(official_reviewers="iz.EditOfficialReviewers") + official_reviewers = schema.Tuple( + title=_("Official reviewers"), + description=_( + "People or groups who may review this item in an official " + "capacity." + ), + value_type=schema.Choice( + title=_("Principal"), + source="plone.principalsource.Principals" + ), + required=False, + missing_value=(), # important! + ) + + directives.widget(unofficial_reviewers=AutocompleteMultiFieldWidget) + directives.write_permission(unofficial_reviewers="iz.EditUnofficialReviewers") + unofficial_reviewers = schema.Tuple( + title=_("Unofficial reviewers"), + description=_( + "People or groups who may review this item in a supplementary " + "capacity" + ), + value_type=schema.Choice( + title=_("Principal"), + source="plone.principalsource.Principals" + ), + required=False, + missing_value=(), # important! + ) + + +class IReviewersMarker(Interface): + """Marker interface that will be provided by instances using the + IReviewers behavior. The ILocalRoleProvider adapter is registered for + this marker. + """ + + +@implementer(ILocalRoleProvider) +@adapter(IReviewersMarker) +class ReviewerLocalRoles(object): + """Grant local roles to reviewers when the behavior is used. + """ + + def __init__(self, context): + self.context = context + + def getRoles(self, principal_id): + """If the user is in the list of reviewers for this item, grant + the Reader, Editor and Contributor local roles. + """ + + c = IReviewers(self.context, None) + if c is None or (not c.official_reviewers and not c.unofficial_reviewers): + return () + + if principal_id in c.official_reviewers: + return ("Reviewer", "OfficialReviewer",) + elif principal_id in c.unofficial_reviewers: + return ("Reviewer",) + + return () + + def getAllRoles(self): + """Return a list of tuples (principal_id, roles), where roles is a + list of roles for the given user id. + """ + + c = IReviewers(self.context, None) + if c is None or (not c.official_reviewers and not c.unofficial_reviewers): + return + + seen = set () + + for principal_id in c.official_reviewers: + seen.add(principal_id) + yield (principal_id, ("Reviewer", "OfficialReviewer"),) + + for principal_id in c.unofficial_reviewers: + if principal_id not in seen: + yield (principal_id, ("Reviewer",),) + + +@implementer(IIndexer) +@adapter(IReviewersMarker, IZCatalog) +class ReviewersIndexer(object): + """Catalog indexer for the "reviewers" index. + """ + + def __init__(self, context, catalog): + self.reviewers = IReviewers(context) + + def __call__(self): + official = self.reviewers.official_reviewers or () + unofficial = self.reviewers.unofficial_reviewers or () + return tuple(set(official + unofficial)) +``` + +Note that the `iz.EditOfficialReviewers` and `iz.EditUnofficialReviewers` permissions are defined and granted elsewhere. + +We need to register these components in {file}`configure.zcml`. + +```xml + + +``` + +This is a quite complex behavior, but hopefully you can see what's going on: + +- There is a standard schema interface, which includes form hints using `plone.autoform.directives` and is marked as an `IFormFieldProvider`. + It uses `plone.formwidget.autocomplete` and `plone.principalsource` to implement the fields. +- We define a marker interface (`IReviewersMarker`) and register this with the `marker` attribute of the `` directive. +- We define and register an adapter from this marker to `ILocalRoles` from `borg.localrole`. +- Similarly, we register a multi-adapter to `IIndexer`, as provided by `plone.indexer`. + +Although this behavior provides a lot of functionality, it is no more difficult for integrators to use than any other. +They would list the behavior interface (`iz.behaviors.reviewers.IReviewers` in this case) in the FTI, and all this functionality comes to life. +This is the true power of behaviors. +Developers can bundle up complex functionality into reusable behaviors, which can then be enabled on a per-type basis by integrators or the same developers in lazier moments. diff --git a/plone.app.dexterity/behaviors/schema-only-behaviors.md b/plone.app.dexterity/behaviors/schema-only-behaviors.md new file mode 100644 index 000000000..03c12647e --- /dev/null +++ b/plone.app.dexterity/behaviors/schema-only-behaviors.md @@ -0,0 +1,114 @@ +--- +myst: + html_meta: + "description": "Schema-only behaviors using annotations or attributes for content types in Plone" + "property=og:description": "Schema-only behaviors using annotations or attributes for content types in Plone" + "property=og:title": "Schema-only behaviors using annotations or attributes for content types in Plone" + "keywords": "Plone, schema-only, behaviors, annotations, attributes, content types" +--- + +# Schema-only behaviors using annotations or attributes + +This chapter describes how to write behaviors that provide schema fields. + +Oftentimes, we simply want a behavior to be a reusable collection of form fields. +Integrators can then compose their types by combining different schemata. +Writing the behavior schema is no different than writing any other schema interface. +But how and where do we store the values? +By default, `plone.behavior` provides two alternatives. + + +## Using annotations + +Annotations, as provided by the [`zope.annotation`](https://pypi.org/project/zope.annotation/) package, are a standard means of storing of key/value pairs on objects. +In the default implementation (so-called `attribute annotation`), the values are stored in a BTree on the object called `__annotations__`. +The raw annotations API involves adapting the object to the `IAnnotations` interface, which behaves like a dictionary, and storing values under unique keys here. +`plone.behavior` comes with a special type of factory that lets you adapt an object to its behavior interface to get an adapter providing this interface, on which you can get and set values, which are eventually stored in annotations. + +We've already seen an example of this factory. + +```xml + +``` + +Here `plone.behavior.AnnotationStorage` is a behavior factory that can be used by any behavior with an interface that consists entirely of `zope.schema` fields. +It stores those items in object annotations, saving you the trouble of writing your own annotation storage adapter. +If you adapt an object for which the behavior is enabled to the behavior interface, you will be able to read and write values off the resultant adapter as usual. + + +## Storing attributes + +This approach is convenient, but there is another approach that is even more convenient, and, contrary to what you may think, may be more efficient: store the attributes of the schema interface directly on the content object. + +As an example, here's the standard `IRelatedItems` behavior from `plone.app.dexerity`. + +```xml + +``` + +The following is the `IRelatedItems` schema. + +```python +from plone.autoform.interfaces import IFormFieldProvider +from plone.autoform.directives import form +from plone.formwidget.contenttree import ObjPathSourceBinder +from plone.supermodel import model +from z3c.relationfield.schema import RelationChoice, +from z3c.relationfield.schema import RelationList +from zope.interface import provider + + +@provider(IFormFieldProvider) +class IRelatedItems(model.Schema): + """Behavior interface to make a type support related items. + """ + + form.fieldset("categorization", label="Categorization", + fields=["relatedItems"]) + + relatedItems = RelationList( + title="Related Items", + default=[], + value_type=RelationChoice(title="Related", + source=ObjPathSourceBinder()), + required=False, + ) +``` + +This is a standard schema using `plone.autoform.directives`. +However, notice the lack of a behavior factory. +This is a directly provided "marker" interface, except that it has attributes, and so it is not actually a marker interface. +The result is that the `relatedItems` attribute will be stored directly onto a content object when first set (usually in the add form). + +This approach has a few advantages: + +- There is no need to write or use a separate factory, so it is a little easier to use. +- The attribute is available on the content object directly, so you can write `context/relatedItems` in a TAL expression, for example. + This does require that it has been set at least once, though. + If the schema is used in the type's add form, that will normally suffice, but old instances of the same type may not have the attribute and could raise an `AttributeError.` +- If the value is going to be used frequently, and especially if it is read when viewing the content object, storing it in an attribute is more efficient than storing it in an annotation. + This is because the `__annotations__` BTree is a separate persistent object which has to be loaded into memory, and may push something else out of the ZODB cache. + +The possible disadvantages are: + +- The attribute name may collide with another attribute on the object, either from its class, its base schema, or another behavior. + Whether this is a problem in practice depends largely on whether the name is likely to be unique. + In most cases, it will probably be sufficiently unique. +- If the attribute stores a large value, it will increase memory usage, as it will be loaded into memory each time the object is fetched from the ZODB. + However, you should use blob to store large values and BTrees to store many values anyway. + Loading an object with a blob or BTree does not mean loading the entire data, so the memory overhead does not occur unless the whole blob or BTree is actually used. + +```{note} +"The moral of this story? BTrees do not always make things more efficient!" ~ Laurence Rowe +``` diff --git a/plone.app.dexterity/behaviors/testing-behaviors.md b/plone.app.dexterity/behaviors/testing-behaviors.md new file mode 100644 index 000000000..5a08d5b00 --- /dev/null +++ b/plone.app.dexterity/behaviors/testing-behaviors.md @@ -0,0 +1,273 @@ +--- +myst: + html_meta: + "description": "How to write unit tests for behaviors for content types in Plone" + "property=og:description": "How to write unit tests for behaviors for content types in Plone" + "property=og:title": "How to write unit tests for behaviors for content types in Plone" + "keywords": "Plone, content types, testing, behaviors" +--- + +# Testing behaviors + +This chapter describes how to write unit tests for behaviors for content types in Plone. + +Behaviors, like any other code, should be tested. +If you write a behavior with just a marker interface or schema interface, it is probably not necessary to test the interface. +However, any actual code, such as a behavior adapter factory, ought to be tested. + +Writing a behavior integration test is not very difficult if you are happy to depend on Dexterity in your test. +You can create a dummy type by instantiating a Dexterity FTI in `portal_types`. +Then enable your behavior by adding its interface name to the `behaviors` property. + +In many cases, however, it is better not to depend on Dexterity at all. +It is not too difficult to mimic what Dexterity does to enable behaviors on its types. +The following example is taken from `collective.gtags` and tests the `ITags` behavior we saw on the first page of this manual. + + +## Behaviors + +This package provides a behavior called `collective.gtags.behaviors.ITags`. +This adds a `Tags` field called `tags` to the `Categorization` fieldset, with a behavior adapter that stores the chosen tags in the `Subject` metadata field. + + +### Test setup + +Before we can run these tests, we need to load the `collective.gtags` configuration. +This will configure the behavior. + +```pycon +>>> configuration = """\ +... +... +... +... +... +... +... """ + +>>> from StringIO import StringIO +>>> from zope.configuration import xmlconfig +>>> xmlconfig.xmlconfig(StringIO(configuration)) +``` + +This behavior can be enabled for any `IDublinCore`. +For the purposes of testing, we will use the `CMFDefault` `Document` type and a custom `IBehaviorAssignable` adapter to mark the behavior as enabled. + +```pycon +>>> from Products.CMFDefault.Document import Document + +>>> from plone.behavior.interfaces import IBehaviorAssignable +>>> from collective.gtags.behaviors import ITags +>>> from zope.component import adapter +>>> from zope.interface import implementer +>>> @adapter(Document) +... @implementer(IBehaviorAssignable) +... class TestingAssignable(object): +... +... enabled = [ITags] +... +... def __init__(self, context): +... self.context = context +... +... def supports(self, behavior_interface): +... return behavior_interface in self.enabled +... +... def enumerate_behaviors(self): +... for e in self.enabled: +... yield queryUtility(IBehavior, name=e.__identifier__) + +>>> from zope.component import provideAdapter +>>> provideAdapter(TestingAssignable) +``` + + +### Behavior installation + +We can now test that the behavior is installed when the ZCML for this package is loaded. + +```pycon +>>> from zope.component import getUtility +>>> from plone.behavior.interfaces import IBehavior +>>> tags_behavior = getUtility(IBehavior, name="collective.gtags.behaviors.ITags") +>>> tags_behavior.interface + +``` + +We also expect this behavior to be a form field provider. +Let's verify that. + +```pycon +>>> from plone.autoform.interfaces import IFormFieldProvider +>>> IFormFieldProvider.providedBy(tags_behavior.interface) +True +``` + + +Using the behavior +------------------ + +Let's create a content object that has this behavior enabled and check that +it works. + +```pycon +>>> doc = Document("doc") +>>> tags_adapter = ITags(doc, None) +>>> tags_adapter is not None +True +``` + +We'll check that the `tags` set is built from the `Subject()` field: + +```pycon +>>> doc.setSubject(["One", "Two"]) +>>> doc.Subject() +("One", "Two") + +>>> tags_adapter.tags == set(["One", "Two"]) +True + +>>> tags_adapter.tags = set(["Two", "Three"]) +>>> doc.Subject() == ("Two", "Three") +True +``` + +This test tries to prove that the behavior is correctly installed and works as intended on a suitable content class. +It is not a true unit test, however. +For a true unit test, we would test the `Tags` adapter directly on a dummy context, but that is not terribly interesting, since all it does is convert sets to tuples. + +First, we configure the package. +To keep the test small, we limit ourselves to the {file}`behaviors.zcml` file, which in this case will suffice. +We still need to include a minimal set of ZCML from `Five`. + +Next, we implement an `IBehaviorAssignable` adapter. +This is a low-level component used by `plone.behavior` to determine if a behavior is enabled on a particular object. +Dexterity provides an implementation that checks the type's FTI. +Our test version is much simpler: it hardcodes the supported behaviors. + +With this in place, we first check that the `IBehavior` utility has been correctly registered. +This is essentially a test to show that we've used the `` directive as intended. +We also verify that our schema interface is an `IFormFieldsProvider`. +For a non-form behavior, we'd omit this. + +Finally, we test the behavior. +We've chosen to use CMFDefault's `Document` type for our test, as the behavior adapter requires an object providing `IDublinCore`. +Ideally, we'd write our own class and implement `IDublinCore` directly. +However, in many cases, the types from `CMFDefault` are going to provide convenient test fodder. + +If our behavior was more complex, we'd add more intricate tests. +By the last section of the doctest, we have enough context to test the adapter factory. + +To run the test, we need a test suite. +Here is our {file}`tests.py`. + +```python +from zope.app.testing import setup +import doctest +import unittest + +def setUp(test): + pass + +def tearDown(test): + setup.placefulTearDown() + +def test_suite(): + return unittest.TestSuite(( + doctest.DocFileSuite( + "behaviors.rst", + setUp=setUp, tearDown=tearDown, + optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS), + )) +``` + +This runs the {file}`behaviors.rst` doctest from the same directory as the {file}`tests.py` file. +To run the test, we can use the usual test runner. + +```shell +./bin/instance test -s collective.gtags +``` + + +## Testing a Dexterity type with a behavior + +Not all behaviors are enabled by default. +Let's say you want to test your Dexterity type when a behavior is enabled or disabled. +To do this, you will need to setup the behavior in your test. +There is an example of this kind of test in the `collective.cover` product. +There is a behavior that adds the capability for the cover page to refresh itself. +The test checks if the behavior is not yet enabled, enables the behavior, check its effect, and then disables it again. + +```python +# -*- coding: utf-8 -*- +from collective.cover.behaviors.interfaces import IRefresh +from collective.cover.interfaces import ICoverLayer +from collective.cover.testing import INTEGRATION_TESTING +from plone import api +from plone.behavior.interfaces import IBehavior +from plone.dexterity.interfaces import IDexterityFTI +from plone.dexterity.schema import SchemaInvalidatedEvent +from zope.component import queryUtility +from zope.event import notify +from zope.interface import alsoProvides + +import unittest + + +class RefreshBehaviorTestCase(unittest.TestCase): + + layer = INTEGRATION_TESTING + + def _enable_refresh_behavior(self): + fti = queryUtility(IDexterityFTI, name="collective.cover.content") + behaviors = list(fti.behaviors) + behaviors.append(IRefresh.__identifier__) + fti.behaviors = tuple(behaviors) + # invalidate schema cache + notify(SchemaInvalidatedEvent("collective.cover.content")) + + def _disable_refresh_behavior(self): + fti = queryUtility(IDexterityFTI, name="collective.cover.content") + behaviors = list(fti.behaviors) + behaviors.remove(IRefresh.__identifier__) + fti.behaviors = tuple(behaviors) + # invalidate schema cache + notify(SchemaInvalidatedEvent("collective.cover.content")) + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + alsoProvides(self.request, ICoverLayer) + with api.env.adopt_roles(["Manager"]): + self.cover = api.content.create( + self.portal, "collective.cover.content", "c1") + + def test_refresh_registration(self): + registration = queryUtility(IBehavior, name=IRefresh.__identifier__) + self.assertIsNotNone(registration) + + def test_refresh_behavior(self): + view = api.content.get_view(u"view", self.cover, self.request) + self.assertNotIn("", view()) + self._enable_refresh_behavior() + self.cover.enable_refresh = True + self.assertIn("", view()) + self.cover.ttl = 5 + self.assertIn("", view()) + self._disable_refresh_behavior() + self.assertNotIn("", view()) +``` + +The methods `_enable_refresh_behavior` and `_disable_refresh_behavior` use the `IDexterityFTI` to get the Factory Type Information for the Dexterity type (`collective.cover.content` in this case). +Then the FTI of `collective.cover.content` is used by both methods to get a list of enabled behaviors. +To enable it, add the desired behavior to the FTI behaviors: `behaviors.append(IRefresh.__identifier__)`. +To disable it, remove the behavior from the FTI behaviors: `behaviors.remove(IRefresh.__identifier__)`. +Assign the resulting behaviors list to the behaviors attribute of the FTI as a tuple: `fti.behaviors = tuple(behaviors)`. +Finally, to have the changes take effect, invalidate the schema cache: `notify(SchemaInvalidatedEvent('collective.cover.content'))`. + + +## A note about marker interfaces + +Marker interface support depends on code that is implemented in Dexterity and is non-trivial to reproduce in a test. +If you need a marker interface in a test, set it manually with `zope.interface.alsoProvides`, or write an integration test with Dexterity content. diff --git a/plone.app.dexterity/custom-views.md b/plone.app.dexterity/custom-views.md new file mode 100644 index 000000000..3252c7f05 --- /dev/null +++ b/plone.app.dexterity/custom-views.md @@ -0,0 +1,297 @@ +--- +myst: + html_meta: + "description": "Configure custom views" + "property=og:description": "Configure custom views" + "property=og:title": "Configure custom views" + "keywords": "Plone, configure, custom views" +--- + +# Custom views + +This chapter describes how to configure custom views and use display forms. + + +## Simple views + +So far our types have used the default views. +They use the display widgets from [`z3c.form`](https://pypi.org/project/z3c.form/), much like the add and edit forms use the edit widgets. +This is functional, but not very attractive. +Most types will need one or more custom view templates. + +Dexterity types are no different from any other content type in Plone. +You can register a view for your schema interface, and it will be available on your type. +If the view is named `view`, it will be the default view, at least if you use the standard {term}`FTI` configuration. +This is because the FTI's `default_view` property is set to `view`, and `view` is in the list of `view_methods`. + +First create a view registration with a `` ZCML directive in your {file}`configure.zcml` file. + +```xml + + + + + +``` + +Next add a browser view in `program.py` as follows. + +```python +from Acquisition import aq_inner +from example.conference.session import ISession +from plone import api +from Products.Five import BrowserView + + +class ProgramView(BrowserView): + + def sessions(self): + """Return a catalog search result of sessions to show.""" + + context = aq_inner(self.context) + catalog = api.portal.get_tool(name='portal_catalog') + + return catalog( + object_provides=ISession.__identifier__, + path='/'.join(context.getPhysicalPath()), + sort_on='sortable_title') +``` + +We have added `sessions`, a helper method which will be used in the view. + +You can add any methods to the view. +They will be available to the template via the `view` variable. +The content object is available via `context`. + +Finaly add a template in {file}`templates/programview.pt`. + +```html + + + + + + +
+ +

+ +
+ + From: + + + + To: + + +
+ +
+ +

+ +

+ +
+ +

Sessions

+
+ +
+ +
+
+ +
+ +
+ + + + + + +``` + +For the most part, this template outputs the values of the various fields, using the `sessions()` method on the view to obtain the sessions contained within the program. + +```{note} +Notice how the `details` `RichText` field is output as `tal:content="structure context/details/output"`. +The `structure` keyword ensures that the rendered HTML is not escaped. +The extra traversal to `details/output` is necessary because the `RichText` field actually stores a `RichTextValue` object that contains not only the raw text as entered by the user, but also a MIME type (for example, `text/html`) and the rendered output text. +`RichText` fields are covered in more detail {ref}`later in this manual `. +``` + +The view for `Presenter` is the following. + +```xml + + + + + +``` + +The template in {file}`templates/presenterview.pt` is similar to the previous template. + +```html + + + + + + +
+ +

+ +
+ +

+ +

+ +
+ +
+ + + + + + +``` + +Obviously, these views are very basic. +Much more interesting views could be created by putting a little more work into the templates. + +You should also realize that you can create any type of view using this technique. +Your view does not have to be related to a particular content type. +You could set the context to `Interface`, for example, to make a view that's available on all types. + + +## Display view + +In this section, we describe how to use display widgets in your views. + +In the previous section, we created a browser view. +This kind of view is the most common. +Sometimes we want to make use of the widgets and information in the type's schema more directly. +For example to invoke transforms or reuse more complex HTML. + +To do this, you can use a display view. +This is a view base class that knows about the schema of a type. +We will use an example in {file}`session.py`, with a template in {file}`templates/sessionview.pt`. + +```{note} +*Display views* involve the same type of overhead as add and edit forms. +If you have complex content type with many behaviors, fieldsets, and widget hints, you may notice a slow down. +This can be a problem on high volume sites. +``` + +The new view class is pretty much the same as before, except that we derive from `plone.dexterity.browser.view.DefaultView`. + +```xml + + + + + +``` + +```python +from plone.dexterity.browser.view import DefaultView + +class SessionView(DefaultView): + pass +``` + +This gives our view a few extra properties that we can use in the template. + +`view.w` +: A dictionary of all the display widgets, keyed by field names. + For fields provided by behaviors, that is usually prefixed with the behavior interface name (`IBehaviorInterface.field_name`). + For the default schema, unqualified names apply. + +`view.widgets` +: Contains a list of widgets in schema order for the default fieldset. + +`view.groups` +: Contains a list of fieldsets in fieldset order. + +`view.fieldsets` +: Contains a dictionary mapping fieldset name to fieldset. + +`widgets` +: On a fieldset (group), you can access a `widgets` list to get widgets in that fieldset. + +The `w` dict is the most commonly used. + +The {file}`templates/sessionview.pt` template contains the following code. + +```html + + + + + +
+

+
+

+

+
+
+ + + + + +``` + +Notice how we use expressions such as `view/w/details/render` (where `details` is the field name) to get the rendering of a widget. +Other properties include `__name__`, the field name, and `label`, the field title. diff --git a/plone.app.dexterity/designing.md b/plone.app.dexterity/designing.md new file mode 100644 index 000000000..1cb08802c --- /dev/null +++ b/plone.app.dexterity/designing.md @@ -0,0 +1,42 @@ +--- +myst: + html_meta: + "description": "Designing with content types in Plone" + "property=og:description": "Designing with content types in Plone" + "property=og:title": "Designing with content types in Plone" + "keywords": "Plone, designing, content types" +--- + +# Designing with content types + +Before we dive into Dexterity, it is worth thinking about the way we design solutions with content types in Plone. +If you are familiar with Archetypes based development, Grok, or Zope 3, then much of this will probably be familiar. + +Plone uses the ZODB, an object database, instead of a relational database as its default content store. +The ZODB is well suited to heterogeneous, loosely structured content such as web pages. + +Types in Plone are either containers or items (this distinction is sometimes called folderish versus non-folderish). +A one-to-many type relationship is typically modeled as a container (the "one") containing many items (the "many"), although it is also possible to use references across the content hierarchy. + +Each type has a schema, which is a set of fields with related properties, such as a title, default value, constraints, and so on. +The schema is used to generate forms and describe instances of the type. +In addition to schema-driven forms, a type typically comes with one or more views, and is subject to security—for example, add permissions, or per-field read/write permissions—and workflow. + +When we attempt to solve a particular content management problem with Plone, we will often design new content types. +For the purpose of this tutorial, we'll build a simple set of types to help conference organizers. +We want to manage a program consisting of multiple sessions. +Each session should be listed against a track, and have a time slot, a title, a description, and a presenter. +We also want to manage biographies for presenters. + +There are many ways to approach this, but here is one possible design: + +- A content type Presenter is used to represent presenter biographies. + Fields include name, description, and professional experience. +- A content type Program represents a given conference program. + Besides some basic metadata, it will list the available tracks. + This type is folderish. +- A content type Session represents a session. + Sessions can only be added inside Programs. + A Session will contain some information about the session, and allow the user to choose the track and associate a presenter. + +Each type will also have custom views, and we will show how to configure catalog indexers, security, and workflow for the types. diff --git a/plone.app.dexterity/index.md b/plone.app.dexterity/index.md new file mode 100644 index 000000000..bd7682b80 --- /dev/null +++ b/plone.app.dexterity/index.md @@ -0,0 +1,36 @@ +--- +myst: + html_meta: + "description": "Plone content types developer manual" + "property=og:description": "Plone content types developer manual" + "property=og:title": "Plone content types developer manual" + "keywords": "Plone, content types, developer, manual" +--- + +# Content types developer manual + +This part of the documentation describes how to develop content types in Plone. + +```{toctree} +:maxdepth: 2 + +intro +designing +prerequisite +schema-driven-types +model-driven-types +custom-views +advanced/index +testing/index +reference/index +``` + + +## Appendices + +```{toctree} +:maxdepth: 1 + +install +behaviors/index +``` diff --git a/plone.app.dexterity/install.md b/plone.app.dexterity/install.md new file mode 100644 index 000000000..f9264f9e3 --- /dev/null +++ b/plone.app.dexterity/install.md @@ -0,0 +1,21 @@ +--- +myst: + html_meta: + "description": "How to install Dexterity for Plone content types" + "property=og:description": "How to install Dexterity for Plone content types" + "property=og:title": "How to install Dexterity for Plone content types" + "keywords": "Plone, Install, Dexterity, content types" +--- + +# Install Dexterity + +This chapter describes how to install Dexterity and use it in your project. + +```{note} +Dexterity is already installed as part of Plone 5.x and later. +No further action is needed for these versions. +``` + +## Install Dexterity in Plone 4.3 + +See [Installing Dexterity](https://4.docs.plone.org/external/plone.app.dexterity/docs/install.html). diff --git a/plone.app.dexterity/intro.md b/plone.app.dexterity/intro.md new file mode 100644 index 000000000..2fb8eaebf --- /dev/null +++ b/plone.app.dexterity/intro.md @@ -0,0 +1,83 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Introduction + +This manual will teach you how to build content types using the Dexterity system. + +If you have decided that Dexterity is for you, and you are a programmer and comfortable +working on the filesystem, then this manual is a good place to start. + +This manual will cover: + +- Some basic design techniques for solving problems with content types in Plone +- Getting a Dexterity development environment set up +- Creating a package to house your types +- Building a custom type based on a schema +- Creating custom views and forms for your type +- Advanced customization, including workflow and security +- Testing your types +- A quick reference to common fields, widgets, and APIs + + +## Why was Dexterity created? + +Dexterity was created to serve two audiences: Administrators/integrators and developers. + +For administrators and integrators, Dexterity offers: + +- the ability to create new content types through-the-web +- the ability to switch on/off various aspects (called "behaviors") on a per-type basis +- improved collaboration between integrators (who may define a type's schema, say) and programmers (who may provide re-usable behaviors that the administrator can plug in). + +For developers, Dexterity promises: + +- the ability to create content types more quickly and easily, and with less boilerplate and repetition, than what is possible with Archetypes or plain CMF types +- content objects with a smaller runtime footprint, to improve performance +- types that use the now-standard `zope.interface`/`zope.schema` style of schema, and more broadly support modern idioms that sit a little awkardly with Archetypes and its ilk + +## How is Dexterity different from Archetypes? + +Dexterity is an alternative to Archetypes, Plone's venerable content type framework. +Being more recent, Dexterity has been able to learn from some of the mistakes that were made Archetypes, and more importantly leverage some of the technologies that did not exist when Archetypes was first conceived. + +Some of the main differences include: + +- Dexterity is able to leverage many technologies that come with newer versions of CMF and Zope 3. + This means that the Dexterity framework contains significantly less code than Archetypes. + Dexterity also has better automated test coverage. +- Dexterity is more modular where Archetypes is more monolithic. + This promises to make it easier to support things like SQL database-backed types, alternative workflow systems, instance-specific sub-types, and so on. + It also means that many of the components developed for Dexterity, such as the through-the-web schema editor, the "behaviors" system, or the forms construction API (`plone.autoform`) are re-usable in other contexts, for example, to build standalone forms or even to augment existing Archetypes-based types. +- Archetypes has its own schema implementation, which is incompatible with the interface-based approached found in `zope.interface` and `zope.schema`. + The latter is used throughout the Zope stack to describe components and build forms. + Various techniques exist to bridge the Archetypes schema to the Zope 3 schema notation, but none are particularly attractive. +- Archetypes uses accessor and mutator methods to get/set values. + These are generated and scribbled onto a class at startup. + Dexterity uses attribute notation. + Whereas in Archetypes you may write `context.getFirstName()`, in Dexterity you would write `context.first_name`. +- Archetypes has its own implementation of fields and widgets. + It is difficult to re-use these in standalone forms or templates, because they are tied to the idea of a content object. + Dexterity uses the de-facto standard `z3c.form` library instead, which means that the widgets used for standalone forms are the same as those used for content type add and edit forms. +- Archetypes does not support add forms. + Dexterity does, via `z3c.form`. + This means that Dexterity types do not need to use the `portal_factory` hack to avoid stale objects in content space, and are thus significantly faster and less error prone. +- Archetypes requires a chunk of boilerplate in your product's `initialize()` method (and requires that your package is registered as a Zope 2 product) and elsewhere. + It requires a particular sequence of initialization calls to register content classes, run the class generator to add accessors/mutators, and set up permissions. + Dexterity does away with all that boilerplate, and tries to minimise repetition. +- It is possible to extend the schemata of existing Archetypes types with the `archetypes.schemaextender` product, although this adds some performance overhead and relies on a somewhat awkward programming technique. + Dexterity types were built to be extensible from the beginning, and it is possible to declaratively turn on or off aspects of a type (such as Dublin Core metadata, locking support, ratings, tagging, and so on) with re-usable "behaviors". +- Dexterity is built from the ground up to support through-the-web type creation. + There are products that achieve the same thing with Archetypes types, but they have to work around a number of limitations in the design of Archetypes that make them somewhat brittle or slow. + Dexterity also allows types to be developed jointly through-the-web and on the filesystem. + For example, a schema can be written in Python and then extended through the web. + +As of version 5 of Plone, Dexterity is the preferred way of creating content types. +Additionally, Archetypes was removed from Plone core in 5.2. +Archetypes can still be added to Plone 5 to support Archetypes-based add-ons, but it will not function when running Plone using Python 3. diff --git a/plone.app.dexterity/model-driven-types.md b/plone.app.dexterity/model-driven-types.md new file mode 100644 index 000000000..6f5180e70 --- /dev/null +++ b/plone.app.dexterity/model-driven-types.md @@ -0,0 +1,116 @@ +--- +myst: + html_meta: + "description": "Model-driven types" + "property=og:description": "Model-driven types" + "property=og:title": "Model-driven types" + "keywords": "Plone, model, content types" +--- + +# Model-driven types + +In the previous section, we defined two types by using Zope schema. +In this section, we're going to define a type's fields using an XML model file. + +The advantage of using a model file is that we can prototype the content type in Dexterity's through-the-web field editor, then export the XML model file for incorporation into our package. + +XML may be used to do pretty much anything you could do via Zope schema. +Many users not already schooled in Zope schema will find this by far the easiest and fastest way to create Dexterity content types. + + +## Setting the field model + +Create an `example/conference/models` directory. +In it, add a `presenter.xml` file with the following content. + +```xml + + + + + Name + + + + A short summary + + + + False + Bio + + + Please upload an image. + False + Photo + + + +``` + +The XML name spaces we use are described in the {doc}`reference/dexterity-xml` reference chapter. + +Open {file}`presenter.py` that we created in the previous chapter, which is a copy of our original {file}`program.py`. +Delete the field declarations from the `IPresenter` class, and edit as shown below: + +```python +from example.conference import MessageFactory as _ +from plone.supermodel import model +from zope import schema + + +class IPresenter(model.Schema): + """Schema for Conference Presenter content type.""" + + model.load("models/presenter.xml") +``` + +Note the `model.load` directive. +This will automatically load our model file to provide the content type field schema. + + +## Setting factory type information + +This part of the process is identical to what we explained in {doc}`schema-driven-types`. + +Look in the {file}`types.xml` file in your package's `example/conference/profiles/default` directory. +It should now contain the following code. + +```xml + + + + + +``` + +For the `Presenter` type, we have `example.conference.presenter.xml`. + +```xml + + + + + Presenter + Conference Presenter + string:file-earmark-text + Presenter + True + True + + False + + + example.conference.presenter.IPresenter + plone.dexterity.content.Item + + + + +``` + +Note that this is may be added anywhere. diff --git a/plone.app.dexterity/prerequisite.md b/plone.app.dexterity/prerequisite.md new file mode 100644 index 000000000..61fa21306 --- /dev/null +++ b/plone.app.dexterity/prerequisite.md @@ -0,0 +1,167 @@ +--- +myst: + html_meta: + "description": "Prerequisites for content types" + "property=og:description": "Prerequisites for content types" + "property=og:title": "Prerequisites for content types" + "keywords": "Plone, content types, prerequisites" +--- + +# Prerequisites + +This portion of the Dexterity documentation mainly intends to illuminate Dexterity features. +If you would like an in-depth, step-by-step approach, we recommend you work through the [Mastering Plone](https://training.plone.org/) training. + + +## Prepare a development environment + +First get a working Plone installation. +If you don't already have one, the easiest way to do so is to use one of Plone's installers. +Note that for development purposes, you may use a standalone (non-ZEO), non-root install. + +Second add our standard development tools. +If you've used one of our installers, developer tool configurations are in a separate file, `develop.cfg`. +Once your site is running, you may activate the development configuration by using the command: + +```shell +bin/buildout -c develop.cfg +``` + +Rather than running `bin/buildout` without arguments. +The `develop.cfg` config file extends the existing `buildout.cfg`. + +The key tools that you'll need, both supplied by `develop.cfg`, are: + +1. `mr.bob`, a Python package skeleton builder; +1. `bobtemplates.plone`, `mr.bob` templates for Plone; and +1. A test runner and code quality testing tools. + +If you've created yor own `buildout.cfg` file rather than using one of the installers, you'll need to add an equivalent development configuration. +The easiest way to do so is to pick up a copy from the [Unified Installer's repository](https://github.com/plone/Installers-UnifiedInstaller/blob/master/base_skeleton/develop.cfg). +To pick up `mr.bob` and the Plone templates alone, just add a `mrbob` part to your buildout: + +```ini +[mrbob] +recipe = zc.recipe.egg +eggs = + mr.bob + bobtemplates.plone +``` + +Don't forget to add `mrbob` to your parts list. + + +## Creating a package + +```{note} +We're going to build a package named `example.conference`. +You may find a completed version of it in the [Collective repository](https://github.com/collective/example.conference). +``` + +Typically, our content types will live in a separate package from our theme and other customizations. + +To create a new package, we can start with `mrbob` and the `dexterity` template. + +```{note} +Nothing that we're doing actually requires `mrbob` or the `bobtemplates.plone` skeleton package. +It's just a quick way of getting started. +``` + +We run the following from the `src/` directory + +```shell +../bin/mrbob bobtemplates.plone:addon -O example.conference +``` + +and specify your target version of Plone and Python. +This will create a directory named `example.conference` inside `./src` with the basic structure of a generic add-on. +Now ["refine"](https://github.com/plone/bobtemplates.plone#provided-subtemplates) it for the creation of a content type. + +```shell +../bin/mrbob bobtemplates.plone:content_type -O example.conference +``` + +Specify `Program` for your content type name, and `Container` as the Dexterity base class (remember that Programs will contain Sessions). +Choose not to use XML Model for this example. + +Now take a look at the `setup.py` file in your new package. +Edit the `author`, `author_email`, and `description` fields as you wish. +Note a couple of parts of the generated `setup.py` file: + +```python +install_requires=[ + + 'plone.app.dexterity', + +], + +entry_points=""" +# -*- Entry points: -*- +[z3c.autoinclude.plugin] +target = plone +""", +``` + +The addition of `plone.app.dexterity` to our install requirements assures that we'll have Dexterity loaded, even in older versions of Plone. +The specification of `plone` as a `z3c.autoinclude.plugin` entry point ensures that we won't need to separately specify our `zcml` in buildout. + +Now let's take a look at `configure.zcml` in the `examples/conference` directory of our project. +Again, we want to note a few parts: + +```xml + + + + + + + + + + + + + +``` + +Here, with the `includeDependencies` tag, we automatically include the ZCML configuration for all packages listed under `install_requires` in `setup.py`. +The alternative would be to manually add a line, such as `` for each dependency. + +The `include package=".browser"` directive loads additional ZCML configuration from the `browser` subdirectory. +In turn, the `browser.resourceDirectory` command in that configuration file creates a directory for static resources that we want to make available through the web. + +Finally, we register a `GenericSetup` profile to make the type installable, which we will build up over the next several sections. + +When you've got your project tuned up, return to your `buildout/instance` directory and edit `buildout.cfg` to add `example.conference` to your eggs list, and `src/example.conference` to your `develop` sources list: + +```ini +eggs = + Plone + + example.conference + + +develop = + + src/example.conference +``` + +Run `bin/buildout -c develop.cfg` to add your new product to the configuration, or just `bin/buildout` if you don't have a separate `develop.cfg`. + +The buildout should now configure Plone, Dexterity, and the `example.conference` package. + +We are now ready to start adding types. diff --git a/plone.app.dexterity/reference/dexterity-xml.md b/plone.app.dexterity/reference/dexterity-xml.md new file mode 100644 index 000000000..fed6ead88 --- /dev/null +++ b/plone.app.dexterity/reference/dexterity-xml.md @@ -0,0 +1,335 @@ +--- +myst: + html_meta: + "description": "Reference for Dexterity's XML name spaces for content types in Plone" + "property=og:description": "Reference for Dexterity's XML name spaces for content types in Plone" + "property=og:title": "Reference for Dexterity's XML name spaces for content types in Plone" + "keywords": "Plone, reference, content types, Dexterity, XML, name spaces" +--- + +# Dexterity XML + +This chapter serves as a reference for Dexterity's XML name spaces. + + +## Introduction + +The schema of a Dexterity content type may be detailed in two very different ways. + +- In Python as a Zope schema +- In XML + +When you use Dexterity's through-the-web (TTW) schema editor, all your work is being saved in the content type's Factory Type Information (FTI) as XML. +`plone.supermodel` dynamically translates that XML into Python objects, which are used to display and edit your content objects. + +The XML model of your content object may be exported from Dexterity and incorporated into a Python package. +That's typically done with code such as the following. + +```python +class IExampleType(form.Schema): + + form.model("models/example_type.xml") +``` + +or: + +```python +from plone.supermodel import xmlSchema + +IExampleType = xmlSchema("models/example_type.xml") +``` + +XML models in a package may be directly edited. + +This document is a reference to the tags and attributes you may use in model XML files. +This includes several form-control and security-control attributes that are not available through the TTW schema editor. + + +## XML document structure + +Dexterity requires that its model XML be well-formed XML, including name space declarations. +The typical structure of a Dexterity XML document is the following. + +```xml + + + + + One + ... More field attributes + + ... More fields + + +``` + +Only the default name space `supermodel/schema` is required for basic schema. +The `supermodel/form` and `supermodel/schema` provide additional attributes to control form presentation and security. + + +## `supermodel/schema` fields + +Most of the `supermodel/schema` field tag and its attributes map directly to what's available via the TTW schema editor: + +```xml + + abc + Test desc + 10 + 2 + m + True + False + Test + +``` + +The field `type` needs to be the full dotted name of the field type, as if it were imported in Python. + + +### Fieldsets + +To add fieldsets, surround embedded `field` tags in a `fieldset` block. + +```xml + + ... +
+ + + Three + + + + Four + +
+ ... +
+``` + + +### Vocabularies + +Vocabularies may be specified via dotted names using the `source` tag. + +```xml + + a + Test desc + + True + False + Test + plone.supermodel.tests.dummy_vocabulary_instance + +``` + +Where the full Python dotted-name of a Zope vocabulary in a package. + +```python +from zope.schema.vocabulary import SimpleVocabulary + +dummy_vocabulary_instance = SimpleVocabulary.fromItems([(1, "a"), (2, "c")]) +``` + +Or, a source binder. + +```xml + + ... + plone.supermodel.tests.dummy_binder + +``` + +Or in Python. + +```python +from zope.schema.interfaces import IContextSourceBinder + +class Binder(object): + implements(IContextSourceBinder) + + def __call__(self, context): + return SimpleVocabulary.fromValues(["a", "d", "f"]) + +dummy_binder = Binder() +``` + +You may also use the `vocabulary` tag rather than `source` to refer to named vocabularies registered via the ZCA. + + +### Internationalization + +Translation domains and message IDs can be specified for text that is interpreted as Unicode. +This will result in deserialization as a `zope.i18nmessageid` message ID rather than a basic Unicode string. + +Note that we need to add the `i18n` namespace and a domain specification. + +```xml + + + + + Title + + + + +``` + + +## `supermodel/form` attributes + +`supermodel/form` provides attributes that govern presentation and editing. + + +### `after/before` + +To reorder fields, use `form:after` or `form:before`. + +The value should be either `"*"`, to put the field first/last in the form, or the name of a another field. +Use `".fieldname"` to refer to a field in the current schema or a base schema. +Use a fully prefixed name (for example, `"my.package.ISomeSchema"`) to refer to a field in another schema. +Use an unprefixed name to refer to a field in the default schema of the form. + +```xml + + One + +``` + + +### `mode` + +To turn a field into a view mode or hidden field, use `form:mode`. +The mode may be set for only some forms by specifying a form interface in the same manner as for `form:omitted`. + +```xml + + Three + +``` + + +### `omitted` + +To omit a field from all forms, use `form:omitted="true"`. +To omit a field only from some forms, specify a form interface such as `form:omitted="z3c.form.interfaces.IForm:true"`. +Multiple `interface:value` settings may be specified, separated by spaces. + +```xml + + One + + + + Three + +``` + +The latter example hides the field on everything except the edit form. + + +### `widget` + +To set a custom widget for a field, use `form:widget` to give a fully qualified name to the field widget factory. + +```xml + + One + +``` + + +### Dynamic defaults + +To set a dynamic default for a field, use a `defaultFactory` tag to give a fully qualified name for a callable. +The defaultFactory callable must provide either `plone.supermodel.interfaces.IDefaultFactory` or `zope.schema.interfaces.IContextAwareDefaultFactory`. + +```xml + + Three + plone.supermodel.tests.dummy_defaultFactory + +``` + +Sample Python for the validator factory. + +```python +@provider(IDefaultFactory) +def dummy_defaultFactory(): + return "something" +``` + +For a callable using context. + +```python +@provider(IContextAwareDefaultFactory) +def dummy_defaultCAFactory(context): + return context.something +``` + +```{versionadded} 4.3.2 +The `defaultFactory` tag was added in `plone.supermodel` 1.2.3, shipping with Plone 4.3.2 and later. +``` + + +### `validator` + +To set a custom validator for a field, use `form:validator` to give a fully qualified name to the field validator factory. +The validator factory should be a class derived from one of the validators in `z3c.form.validator`. + +```xml + + Three + +``` + +Sample Python for the validator factory. + +```python +class TestValidator(z3c.form.validator.SimpleFieldValidator): + + def validate(self, value): + super(TestValidator, self).validate(value) + raise Invalid("Test") +``` + + +(dexterity-xml-security)= + +## `supermodel/security` attributes + + +### `read-permission` and `write-permission` + +To set a read or write permission, use `security:read-permission` or `security:write-permission`. +The value should be the name of an `IPermission` utility. + +```xml + + One + +``` diff --git a/plone.app.dexterity/reference/fields.md b/plone.app.dexterity/reference/fields.md new file mode 100644 index 000000000..762791d28 --- /dev/null +++ b/plone.app.dexterity/reference/fields.md @@ -0,0 +1,122 @@ +--- +myst: + html_meta: + "description": "Standard schema fields in Plone content types" + "property=og:description": "Standard schema fields in Plone content types" + "property=og:title": "Standard schema fields in Plone content types" + "keywords": "Plone, schema fields, content types" +--- + +# Fields + +This chapter describes the standard schema fields in Plone content types. + +The following tables show the most common field types for use in Dexterity schemata. +See the documentation on {ref}`creating schemata ` for information about how to use these. + + +## Field properties + +Fields are initialized with properties passed in their constructors. +To avoid having to repeat the available properties for each field, we'll list them once here, grouped into the interfaces that describe them. +You'll see those interfaces again in the tables below that describe the various field types. +Refer to the table below to see what properties a particular interface implies. + +| Interface | Property | Type | Description | +| - | - | - | - | +| IField | title | int | The title of the field. Used in the widget. | +| | `description` | unicode | A description for the field. Used in the widget. | +| | `required` | bool | Whether or not the field is required. Used for form validation. The default is `True`. | +| | `readonly` | bool | Whether or not the field is read only. Default is `False`. | +| | `default` | | The default value for the field. Used in forms and sometimes as a fallback value. Must be a valid value for the field if set. The default is `None`. | +| | `missing_value` | | A value that represents "this field is not set". Used by form validation. Defaults to `None`. For lists and tuples, it is sometimes useful to set this to an empty list or tuple. | +| `IMinMaxLen` | `min_length` | int | The minimum required length or minimum number of elements. Used for `string`, sequence, mapping, or `set` fields. Default is `0`. | +| | `max_length `| int | The maximum allowed length or maximum number of elements. Used for `string`, sequence, mapping, or `set` fields. Default is `None` (no check). | +| `IMinMax` | `min` | | The minimum allowed value. Must be a valid value for the field, for example, an int field should be an integer. Default is `None` (no check). | +| | `max` | | The maximum allowed value. Must be a valid value for the field, for example an int field should be an integer. Default is `None` (no check). | +| `ICollection` | `value_type` | | Another `Field` instance that describes the allowable values in a list, tuple, or other collection. Must be set for any collection field. One common usage is to set this to a `Choice` to model a multi-selection field with a vocabulary. | +| | `unique` | bool | Whether or not values in the collection must be unique. Usually not set directly. Use a `Set` or `Frozenset` to guarantee uniqueness in an efficient way. | +| `IDict` | `key_type` | | Another `Field` instance that describes the allowable keys in a dictionary. Similar to the `value_type` of a collection. Must be set. | +| | `value_type` | | Another `Field` instance that describes the allowable values in a dictionary. Similar to the `value_type` of a collection. Must be set. | +| `IObject` | `schema` | `Interface` | An interface that must be provided by any object stored in this field. | +| `IRichText` | `default_mime_type` | str | Default MIME type for the input text of a rich text field. Defaults to `text/html`. | +| | `output_mime_type` | str | Default output MIME type for the transformed value of a rich text field. Defaults to `text/x-html-safe`. There must be a transformation chain in the `portal_transforms` tool that can transform from the input value to the `output` value for the output property of the `RichValue` object to contain a value. | +| | `allowed_mime_types` | tuple | A list of allowed input MIME types. The default is `None`, in which case the site-wide settings from the {guilabel}`Markup` control panel will be used. | + + +## Field types + +The following tables describe the most commonly used field types, grouped by the module from which they can be imported. + + +### Fields in `zope.schema` + +| Name | Type | Description | Properties | +| - | - | - | - | +| Choice | N/A | Used to model selection from a vocabulary, which must be supplied. Often used as the `value_type` of a selection field. The value type is the value of the terms in the vocabulary. | See {doc}`../advanced/vocabularies`. | +| Bytes | str | Used for binary data. | IField, IMinMaxLen | +| ASCII | str | ASCII text (multi-line). | IField, IMinMaxLen | +| BytesLine | str | A single line of binary data, in other words a `Bytes` with new lines disallowed. | IField, IMinMaxLen | +| ASCIILine | str | A single line of ASCII text. | IField, IMinMaxLen | +| Text | unicode | Unicode text (multi-line). Often used with a WYSIWYG widget, although the default is a text area. | IField, IMinMaxLen | +| TextLine | unicode | A single line of Unicode text. | IField, IMinMaxLen | +| Bool | bool | `True` or `False`. | IField | +| Int | int, long | An integer number. Both ints and longs are allowed. | IField, IMinMax | +| Float | float | A floating point number. | IField, IMinMax | +| Tuple | tuple | A tuple (non-mutable). | IField, ICollection, IMinMaxLen | +| List | list | A list. | IField, ICollection, IMinMaxLen | +| Set | set | A set. | IField, ICollection, IMinMaxLen | +| Frozenset | frozenset | A frozenset (non-mutable). | IField, ICollection, IMinMaxLen | +| Password | unicode | Stores a simple string, but implies a password widget. | IField, IMinMaxLen | +| Dict | dict | Stores a dictionary. Both `key_type` and `value_type` must be set to fields. | IField, IMinMaxLen, IDict | +| Datetime | datetime | Stores a Python `datetime` (not a Zope 2 `DateTime`). | IField, IMinMax | +| Date | date | Stores a python `date`. | IField, IMinMax | +| Timedelta | timedelta | Stores a python `timedelta`. | IField, IMinMax | +| SourceText | unicode | A textfield intended to store source text, such as HTML or Python code. | IField, IMinMaxLen | +| Object | N/A | Stores a Python object that conforms to the interface given as the `schema`. There is no standard widget for this. | IField, IObject | +| URI | str | A URI (URL) string. | IField, MinMaxLen | +| Id | str | A unique identifier, either a URI or a dotted name. | IField, IMinMaxLen | +| DottedName | str | A dotted name string. | IField, IMinMaxLen | +| InterfaceField | Interface | A Zope interface. | IField | +| Decimal | Decimal | Stores a Python `Decimal`. Requires version 3.4 or later of [`zope.schema`](https://pypi.org/project/zope.schema/). Not available by default in Zope 2.10. | IField, IMinMax | + + +### Fields in `plone.namedfile.field` + +See [`plone.namedfile`](https://pypi.org/project/plone.namedfile/) and [plone.formwidget.namedfile](https://pypi.org/project/plone.formwidget.namedfile/) for more details. + +| Name | Type | Description | Properties | +| - | - | - | - | +| NamedFile | NamedFile | A binary uploaded file. Normally used with the widget from `plone.formwidget.namedfile`. | IField | +| NamedImage | NamedImage | A binary uploaded image. Normally used with the widget from `plone.formwidget.namedfile`. | IField | +| NamedBlobFile | NamedBlobFile | A binary uploaded file stored as a ZODB blob. Requires the `blobs` extra to `plone.namedfile`. Otherwise identical to `NamedFile`. | IField | +| NamedBlobImage | NamedBlobImage | A binary uploaded image stored as a ZODB blob. Requires the `blobs` extra to `plone.namedfile`. Otherwise identical to `NamedImage`. | IField | + + +### Fields in `z3c.relationfield.schema` + +See [`z3c.relationfield`](https://pypi.org/project/z3c.relationfield/) for more details. + +| Name | Type | Description | Properties | +| -------------- | ------------- | ------------------------------------------------------------ | ------------ | +| Relation | RelationValue | Stores a single `RelationValue`. | IField | +| RelationList | list | A `List` field that defaults to `Relation` as the value type | See `List` | +| RelationChoice | RelationValue | A `Choice` field intended to store `RelationValue`'s | See `Choice` | + + +### Fields in `plone.app.textfield` + +See [`plone.app.textfield`](https://pypi.org/project/plone.app.textfield/) for more details. + +| Name | Type | Description | Properties | +| -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | +| RichText | RichTextValue | Stores a `RichTextValue`, which encapsulates a raw text value, the source MIME type, and a cached copy of the raw text transformed to the default output MIME type. | IField, IRichText | + + +### Fields in `plone.schema` + +See [`plone.schema`](https://pypi.org/project/plone.schema/) for more details. + +| Name | Type | Description | Properties | +| ----- | ---- | ----------------------------------- | ------------------ | +| Email | str | A field containing an email address | IField, IMinMaxLen | diff --git a/plone.app.dexterity/reference/form-schema-hints.md b/plone.app.dexterity/reference/form-schema-hints.md new file mode 100644 index 000000000..ffa27cdb0 --- /dev/null +++ b/plone.app.dexterity/reference/form-schema-hints.md @@ -0,0 +1,160 @@ +--- +myst: + html_meta: + "description": "Form configuration with schema hints using directives in Plone" + "property=og:description": "Form configuration with schema hints using directives in Plone" + "property=og:title": "Form configuration with schema hints using directives in Plone" + "keywords": "Plone, form, configuration, schema, hints, directives" +--- + +# Form configuration with schema hints using directives + +Dexterity uses the directives in [`plone.autoform`](https://pypi.org/project/plone.autoform/) and [`plone.supermodel`](https://pypi.org/project/plone.supermodel/) packages to configure its [`z3c.form`](https://z3cform.readthedocs.io/en/latest/)-based add and edit forms. +A directive annotates a schema with "form hints", which are used to configure the form when it gets generated from the schema. + +The easiest way to apply form hints in Python code is to use the directives from [`plone.autoform`](https://pypi.org/project/plone.autoform/) and `plone.supermodel`. +For the directives to work, the schema must derive from `plone.supermodel.model.Schema`. + +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 to which they apply. + +For example, here is a schema that omits a field. + +```python +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() +``` + +The form directives take parameters in the form of a list of field names, or a set of field name/value pairs as keyword arguments. +Each directive can be used zero or more times. + +There are two kinds of directives. + +- Appearance related directives +- Security related directives + + +## Appearance related directives + +`plone.autoform.directives` provides the following. + +| Name | Description | +| - | -| +| widget | Specify an alternate widget for a field. Pass the field name as a key and a widget as the value. The widget can either be a `z3c.form` widget instance or a string giving the dotted name to one. | +| omitted | Omit one or more fields from forms. Takes a sequence of field names as parameters. | +| mode | Set the widget mode for one or more fields. Pass the field name as a key and the string `"input"`, `"display"`, or `"hidden"` as the value. | +| order_before | Specify that a given field should be rendered before another. Fields can only be ordered if they are in the same fieldset, otherwise order directive is ignored. Pass the field name as a key and name of the other field as a value. If the other field is in a supplementary schema such as from a behavior, then its name will be, for example `IOtherSchema.other_field_name`. If the other field is from the same schema, its name can be abbreviated by a leading dot, for example `.other_field_name`. If the other field is is used without a prefix, it is looked up from the main schema, for example `other_field_name`. Alternatively, pass the string `*` to put a field first in the form's fieldset. | +| order_after | The inverse of `order_before()`, putting a field after another. It works almost similar to `order_before`, except passing `*` will put the field at the end of the fieldsets form. | + +`plone.supermodel.directives` provides the following. + +| Name | Description | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| fieldset | Creates a new (or reuses an existing) fieldset (rendered in Plone as a tab on the edit form). | +| primary | Designate a given field as the primary field in the schema. This is not used for form rendering, but is used for WebDAV marshaling of the content object. | + +The code sample below illustrates each of these directives. + +```python +from plone.autoform import directives +from plone.supermodel.directives import fieldset +from plone.supermodel.directives import primary +from plone.supermodel import model +from plone.app.z3cform.wysiwyg import WysiwygFieldWidget +from zope import schema + + +class ISampleSchema(model.Schema): + + # A fieldset with id "extra" and label "Extra information" containing + # the "footer" and "dummy" fields. The label can be omitted if the + # fieldset has already been defined. + + fieldset("extra", + label="Extra information", + fields=["footer", "dummy"] + ) + + # Here a widget is specified as a dotted name. + # The body field is also designated as the primary field for this schema + + directives.widget(body="plone.app.z3cform.wysiwyg.WysiwygFieldWidget") + primary("body") + body = schema.Text( + title="Body text", + required=False, + default="Body text goes here" + ) + + # The widget can also be specified as an object + + directives.widget(footer=WysiwygFieldWidget) + footer = schema.Text( + title="Footer text", + required=False + ) + + # An omitted field. + # Use directives.omitted("a", "b", "c") to omit several fields + + directives.omitted("dummy") + dummy = schema.Text( + title="Dummy" + ) + + # A field in "hidden" mode + + directives.mode(secret="hidden") + secret = schema.TextLine( + title="Secret", + default="Secret stuff" + ) + + # This field is moved before the "description" field of the standard + # IDublinCore behavior, if this is in use. + + directives.order_before(importantNote="IDublinCore.description") + importantNote = schema.TextLine( + title="Important note", + ) +``` + + +## Security related directives + +The security directives in the `plone.autoform.directives` module are shown below. +Note that these are also used to control reading and writing of fields on content instances. + +| Name | Description | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| read_permission | Set the name (ZCML-style) of a permission required to read the field's value. Pass the field name as a key and the permission name as a string value. Among other things, this controls the field's appearance in display forms. | +| write_permission | Set the name (ZCML-style) of a permission required to write the field's value. Pass the field name as a key and the permission name as a string value. Among other things, this controls the field's appearance in add and edit forms. | + +The code sample below illustrates each of these directives. + +```python +from plone.autoform import directives +from plone.supermodel import model +from zope import schema + +class ISampleSchema(model.Schema): + + # This field requires the "cmf.ReviewPortalContent" permission + # to be read and written + + directives.read_permission(reviewNotes="cmf.ReviewPortalContent") + directives.write_permission(reviewNotes="cmf.ReviewPortalContent") + reviewNotes = schema.Text( + title="Review notes", + required=False, + ) +``` diff --git a/plone.app.dexterity/reference/index.md b/plone.app.dexterity/reference/index.md new file mode 100644 index 000000000..5fe67cf7a --- /dev/null +++ b/plone.app.dexterity/reference/index.md @@ -0,0 +1,24 @@ +--- +myst: + html_meta: + "description": "Useful references for field types, widgets, and APIs for content types in Plone" + "property=og:description": "Useful references for field types, widgets, and APIs for content types in Plone" + "property=og:title": "Useful references for field types, widgets, and APIs for content types in Plone" + "keywords": "Plone, reference, field types, widgets, and APIs" +--- + +# Reference + +This part of the documentation describes useful references for field types, widgets, and APIs. + +```{toctree} +:maxdepth: 2 + +fields +widgets +standard-behaviors +form-schema-hints +manipulating-content-objects +dexterity-xml +misc +``` diff --git a/plone.app.dexterity/reference/manipulating-content-objects.md b/plone.app.dexterity/reference/manipulating-content-objects.md new file mode 100644 index 000000000..f211eca4f --- /dev/null +++ b/plone.app.dexterity/reference/manipulating-content-objects.md @@ -0,0 +1,361 @@ +--- +myst: + html_meta: + "description": "Manipulating content objects in Plone" + "property=og:description": "Manipulating content objects in Plone" + "property=og:title": "Manipulating content objects in Plone" + "keywords": "Plone, manipulating, content objects" +--- + +# Manipulating content objects + +This chapter describes common APIs used to manipulate Dexterity content objects. + +```{note} +Here the low level API is shown. +When writing Plone add-ons, consider using `plone.api` because it covers several standard cases and is a simple, future-proof, and stable API. +``` + +In this section, we will describe some of the more commonly used APIs that can be used to inspect and manipulate Dexterity content objects. +In most cases, the content object is referred to as `context`, its parent folder is referred to as `folder`, and the type name is `example.type`. +Relevant imports are shown with each code snippet, though of course you are more likely to place those at the top of the relevant code module. + + +## Content object creation and folder manipulation + +This section describes the means to create objects and manipulate folders. + + +### Creating a content object + +The simplest way to create a content item is via its factory: + +```python +from zope.component import createObject +context = createObject("example.type") +``` + +At this point, the object is not wrapped with an acquisition. +You can wrap it explicitly by calling the following. + +```python +wrapped = context.__of__(folder) +``` + +However, it's normally better to add the item to a folder and then re-get it from the folder. + +Note that the factory is normally installed as a local utility, so the `createObject()` call will only work once you've traversed over the Plone site root. + +There is a convenience method that can be used to create a Dexterity object. +It is mostly useful in tests. + +```python +from plone.dexterity.utils import createContent +context = createContent("example.type", title="Foo") +``` + +Any keyword arguments are used to set properties on the new instance via `setattr()` on the newly created object. +This method relies on being able to look up the FTI as a local utility, so again you must be inside the site for it to work. + + +### Adding an object to a container + +Once an object has been created, it can be added to a container. +If the container is a Dexterity container, or another container that supports a dict API (for example, a `Large Plone Folder` in Plone 3, or a container based on `plone.folder`), you can do the following. + +```python +folder["some_id"] = context +``` + +You should normally make sure that the `id` property of the object is the same as the ID used in the container. + +If the object only supports the basic OFS API (as is the case with standard Plone `Folders` in Plone 3), you can use the `_setObject()` method. + +```python +folder._setObject("some_id") = context +``` + +Note that both of these approaches bypass any type checks, in other words, you can add items to containers that would not normally allow this type of content. +Dexterity comes with a convenience function, useful in tests, to simulate the checks performed when content is added through the web. + +```python +from plone.dexterity.utils import addContentToContainer +addContentToContainer(folder, context) +``` + +This will also invoke a name chooser and set the object's ID accordingly. +Things such as the `title-to-id` behavior should work. +As before, this relies on local components, so you must have traversed into a Plone site. +`PloneTestCase` takes care of this for you. + +To bypass folder constraints, you can use this function and pass `checkConstraints=False`. + +You can also both create and add an object in one call. + +```python +from plone.dexterity.utils import createContentInContainer +createContentInContainer(folder, "example.type", title="Foo") +``` + +Again, you can pass `checkConstraints=False` to bypass folder constraints, and pass object properties as keyword arguments. + +Finally, you can use the `invokeFactory()` API, which is similar but more generic, in that it can be used for any type of content, not just Dexterity content. + +```python +new_id = folder.invokeFactory("example.type", "some_id") +context = folder["new_id"] +``` + +This always respects add constraints, including add permissions and the current user's roles. + + +### Getting items from a folder + +Dexterity containers and other containers based on `plone.folder` support a dict-like API to obtain and manipulate items in folders. +For example, to obtain an acquisition-wrapped object by name. + +```python +context = folder["some_id"] +``` + +Folders can also be iterated over. +You can call `items()`, `keys()`, `values()`, and so on, treating the folder as a dict with string keys and content objects as values. + +Dexterity containers also support the more basic OFS API. +You can call `objectIds()` to get keys, `objectValues()` to get a list of content objects, `objectItems()` to get an `items()`-like dict, and `hasObject(id)` to check if an object exists in a container. + + +### Removing items from a folder + +Again, Dexterity containers act like dictionaries, and thus implement `__delitem__`. + +```python +del folder["some_id"] +``` + +The OFS API uses the `_delObject()` function for the same purpose. + +```python +folder._delObject("some_id") +``` + + +## Object introspection + +This section describes the means of getting information about an object. + + +### Obtaining an object's schema interface + +A content object's schema is an interface, in other words, an object of type `zope.interface.interface.InterfaceClass`. + +```python +from zope.app.content import queryContentType +schema = queryContentType(context) +``` + +The schema can now be inspected. + +```python +from zope.schema import getFieldsInOrder +fields = getFieldsInOrder(schema) +``` + + +### Finding an object's behaviors + +To find all behaviors supported by an object, use the `plone.behavior` API. + +```python +from plone.behavior.interfaces import IBehaviorAssignable +assignable = IBehaviorAssignable(context) +for behavior in assignable.enumerateBehaviors(): + behavior_schema = behavior.interface + adapted = behavior_schema(context) + # ... +``` + +The objects returned are instances providing `plone.behavior.interfaces.IBehavior`. +To get the behavior schema, use the `interface` property of this object. +You can inspect this and use it to adapt the context if required. + + +### Getting the FTI + +To obtain a Dexterity FTI, look it up as a local utility. + +```python +from zope.component import getUtility +from plone.dexterity.interfaces import IDexterityFTI +fti = getUtility(IDexterityFTI, name="example.type") +``` + +The returned object provides `plone.dexterity.interfaces.IDexterityFTI`. +To get the schema interface for the type from the FTI, you can do the following. + +```python +schema = fti.lookupSchema() +``` + + +### Getting the object's parent folder + +A Dexterity item in a Dexterity container should have the `__parent__` property set, pointing to its containing parent. + +```python +folder = context.__parent__ +``` + +Items in standard Plone folders won't have this property set, at least not in Plone 3.x. + +The more general approach relies on acquisition. + +```python +from Acquisition import aq_inner, aq_parent +folder = aq_parent(aq_inner(context)) +``` + + +## Workflow + +This section describes ways to inspect an object's workflow state and invoke transitions. + + +### Obtaining the workflow state of an object + +To obtain an object's workflow state, ask the `portal_workflow` tool: + +```python +from Products.CMFCore.utils import getToolByName +portal_workflow = getToolByName(context, "portal_workflow") +review_state = portal_workflow.getInfoFor(context, "review_state") +``` + +This assumes that the workflow state variable is called `review_state`, as is the case for almost all workflows. + + +### Invoking a workflow transition + +To invoke a transition, use the following. + +```python +portal_workflow.doActionFor(context, "some_transition") +``` + +The transition must be available in the current workflow state, for the current user. +Otherwise, an error will be raised. + + +## Cataloging and indexing + +This section describes ways of indexing an object in the `portal_catalog` tool. + + +### Reindexing the object + +Objects may need to be reindexed if they are modified in code. +The best way to reindex them is to send an event, and let Dexterity's standard event handlers take care of this. + +```python +from zope.lifecycleevent import modified +modified(context) +``` + +In tests, it is sometimes necessary to reindex explicitly. +This can be done with the following. + +```python +context.reindexObject() +``` + +You can also pass specific index names to reindex, if you don't want to reindex everything. + +```python +context.reindexObject(idxs=["Title", "sortable_title"]) +``` + +This method comes from the `Products.CMFCore.CMFCatalogAware.CMFCatalogAware` mix-in class. + + +## Security + +This section describes ways to check and modify permissions. +For more information, see the section on {doc}`../advanced/permissions`. + + +### Checking a permission + +To check a permission by its Zope 3 name, use the following. + +```python +from zope.security import checkPermission +checkPermission("zope2.View", context) +``` + +```{note} +In a test, you may get an `AttributeError` when calling this method. +To resolve this, call `newInteraction()` from `Products.Five.security` in your test setup (for example, the `afterSetUp()` method). +``` + +Use the Zope 2 permission title. + +```python +from AccessControl import getSecurityManager +getSecurityManager().checkPermission("View", context) +``` + +Sometimes, normally in tests, you want to know which roles have a particular permission. +To do this, use the following. + +```python +roles = [r["name"] for r in context.rolesOfPermission("View") if r["selected"]] +``` + +Again, note that this uses the Zope 2 permission title. + + +### Changing permissions + +Normally, permissions should be set with workflow, but in tests it is often useful to manipulate security directly. + +```python +context.manage_permission("View", roles=["Manager", "Owner"], acquire=True) +``` + +Again note that this uses the Zope 2 permission title. + + +## Content object properties and methods + +The following table shows the more important properties and methods available on Dexterity content objects. +In addition, any field described in the type's schema will be available as a property, and can be read and set using normal attribute access. + +| Property/method | Type | Description | +| -------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| __name__ | unicode | The name (ID) of the object in its container. This is a Unicode string to be consistent with the Zope 3 `IContained` interface, although in reality it will only ever contain ASCII characters, since Zope 2 does not support non-ASCII URLs. | +| id | str | The name (ID) of the object in its container. This is an ASCII string encoding of the `__name__`. | +| getId() | str | Returns the value of the `id` `property`. +| isPrincipiaFolderish | bool/int | `True` (or `1`) if the object is a folder. `False` (or `0`) otherwise. | +| portal_type | str | The `portal_type` of this instance. Should match an FTI in the `portal_types` tool. For Dexterity types, should match a local utility providing `IDexterityFTI`. Note that the `portal_type` is a per-instance property set upon creation (by the factory), and should not be set on the class. | +| meta_type | str | A Zope 2 specific way to describe a class. Rarely, if ever, used in Dexterity. Do not set it on your own classes unless you know what you're doing. | +| title_or_id() | str | Returns the value of the `title` property or, if this is not set, the `id` property. | +| absolute_url() | str | The full URL to the content object. Will take virtual hosting and the current domain into account. | +| getPhysicalPath() | tuple | A sequence of string path elements from the application root. Stays the same regardless of virtual hosting and domain. A common pattern is to use `'/'.join(context.getPhysicalPath())` to get a string representing the path to the Zope application root. Note that it is *not* safe to construct a relative URL from the path, because it does not take virtual hosting into account. | +| title | unicode/str | Property representing the title of the content object. Usually part of an object's schema or provided by the `IBasic` behavior. The default is an empty string. | +| Title() | unicode/str | Dublin Core accessor for the `title` property. Set the title by modifying this property. You can also use `setTitle()`. | +| listCreators() | tuple | A list of user IDs for object creators. The first creator is normally the owner of the content object. You can set this list using the `setCreators()` method. | +| Creator() | str | The first creator returned by the `listCreators()` method. Usually the owner of the content object. | +| Subject() | tuple | Dublin Core accessor for item keywords. You can set this list using the `setSubject()` method. | +| Description() | unicode/str | Dublin Core accessor for the `description` property, which is usually part of an object's schema or provided by the `IBasic` behavior. You can set the description by setting the `description` attribute, or using the `setDescription()` method. | +| listContributors() | tuple | Dublin Core accessor for the list of object contributors. You can set this with `setContributors()`. | +| Date() | str | Dublin Core accessor for the default date of the content item, in ISO format. Uses the effective date is set, falling back on the modification date. | +| CreationDate() | str | Dublin Core accessor for the creation date of the content item, in ISO format. | +| EffectiveDate() | str | Dublin Core accessor for the effective publication date of the content item, in ISO format. You can set this by passing a DateTime object to `setEffectiveDate()`. | +| ExpirationDate() | str | Dublin Core accessor for the content expiration date, in ISO format. You can set this by passing a DateTime object to `setExpirationDate()`. | +| ModificationDate() | str | Dublin Core accessor for the content last-modified date, in ISO format. | +| Language() | str | Dublin Core accessor for the content language. You can set this using `setLanguage()`. | +| Rights() | str | Dublin Core accessor for content copyright information. You can set this using `setRights()`. | +| created() | DateTime | Returns the Zope 2 DateTime for the object's creation date. If not set, returns a "floor" date of January 1, 1970. | +| modified() | DateTime | Returns the Zope 2 DateTime for the object's modification date. If not set, returns a "floor" date of January 1, 1970. | +| effective() | DateTime | Returns the Zope 2 DateTime for the object's effective date. If not set, returns a "floor" date of January 1, 1970. | +| expires() | DateTime | Returns the Zope 2 DateTime for the object's expiration date. If not set, returns a "floor" date of January 1, 1970. | diff --git a/plone.app.dexterity/reference/misc.md b/plone.app.dexterity/reference/misc.md new file mode 100644 index 000000000..babcfc57e --- /dev/null +++ b/plone.app.dexterity/reference/misc.md @@ -0,0 +1,26 @@ +--- +myst: + html_meta: + "description": "Miscellaneous contributor contributed recipes for content types and schema in Plone" + "property=og:description": "Miscellaneous contributor contributed recipes for content types and schema in Plone" + "property=og:title": "Miscellaneous contributor contributed recipes for content types and schema in Plone" + "keywords": "Plone, miscellaneous, recipes, content types" +--- + +# Miscellaneous recipes + +## Hiding a field + +On occasion you may want to hide a field in a schema without modifying the original schema. +To do this, you can use tagged values on the schema. +In this example, you would hide the `introduction` and `answers` fields: + +```python +from example.package.content.assessmentitem import IAssessmentItem +from plone.autoform.interfaces import OMITTED_KEY +IAssessmentItem.setTaggedValue(OMITTED_KEY, + [(Interface, "introduction", "true"), + (Interface, "answers", "true")]) +``` + +This code can reside in {file}`another.package.__init__.py`. diff --git a/plone.app.dexterity/reference/standard-behaviors.md b/plone.app.dexterity/reference/standard-behaviors.md new file mode 100644 index 000000000..c9362c077 --- /dev/null +++ b/plone.app.dexterity/reference/standard-behaviors.md @@ -0,0 +1,38 @@ +--- +myst: + html_meta: + "description": "Standard behaviors of content types in Plone" + "property=og:description": "Standard behaviors of content types in Plone" + "property=og:title": "Standard behaviors of content types in Plone" + "keywords": "Plone, standard, behaviors, content types" +--- + +# Standard behaviors + +This chapter lists common behaviors that ship with Plone and Dexterity. + +Plone and Dexterity ships with several standard behaviors. +The following table shows the shortnames you can list in the FTI `behaviors` properties and the resultant form fields and interfaces. + +| Short Name | Interface | Description | +| - | - | - | +| plone.allowdiscussion | plone.app.dexterity.behaviors.discussion.IAllowDiscussion | Allow discussion on this item. | +| plone.basic | plone.app.dexterity.behaviors.metadata.IBasic | Basic metadata: Adds `title` and `description` fields. | +| plone.categorization | plone.app.dexterity.behaviors.metadata.ICategorization | Categorization: Adds `keywords` and `language` fields. | +| plone.collection | plone.app.contenttypes.behaviors.collection.Collection | Collection behavior with `querystring` and other related fields. | +| plone.dublincore | plone.app.dexterity.behaviors.metadata.IDublinCore | Dublin Core metadata: Adds standard metadata fields. Shortcut for (and same as) `plone.basic` + `plone.categorization` + `plone.publication` + `plone.ownership`) | +| plone.excludefromnavigation | plone.app.dexterity.behaviors.exclfromnav.IExcludeFromNavigation | Exclude From navigation: Allow items to be excluded from navigation. | +| plone.leadimage | plone.app.contenttypes.behaviors.leadimage.ILeadImage | Adds a `LeadImage` field like used for News item. | +| plone.namefromfilename | plone.app.dexterity.behaviors.filename.INameFromFileName | Name from file name: Automatically generate short URL name for content based on its primary field file name. Not a form field provider. | +| plone.namefromtitle | plone.app.content.interfaces.INameFromTitle | Name from title: Automatically generate short URL name for content based on its initial title. Not a form field provider. | +| plone.navigationroot | plone.app.layout.navigation.interfaces.INavigationRoot | Navigation root: Make all items of this type a navigation root. Not a form field provider. | +| plone.nextpreviousenabled | plone.app.dexterity.behaviors.nextprevious.INextPreviousEnabled | Next/previous navigation: Enable next/previous navigation for all items of this type. Not a form field provider. | +| plone.nextprevioustoggle | plone.app.dexterity.behaviors.nextprevious.INextPreviousToggle | Next/previous navigation toggle: Allow items to have next/previous navigation enabled. | +| plone.ownership | plone.app.dexterity.behaviors.metadata.IOwnership | Ownership: Adds creator, contributor, and rights fields. | +| plone.publication | plone.app.dexterity.behaviors.metadata.IPublication | Date range for publication: Adds effective date and expiration date fields. | +| plone.relateditems | plone.app.relationfield.behavior.IRelatedItems | Adds the `Related items` field to the `Categorization` fieldset. | +| plone.richtext | plone.app.contenttypes.behaviors.richtext.IRichText | Rich text field with a WYSIWIG editor. | +| plone.selectablecontrainstypes | Products.CMFPlone.interfaces.constrains.ISelectableConstrainTypes | Folder Addable Constrains: Restrict the content types that can be added to folderish content. | +| plone.shortname | plone.app.dexterity.behaviors.id.IShortName | Short name: Gives the ability to rename an item from its edit form. | +| plone.tableofcontents | plone.app.contenttypes.behaviors.tableofcontents.ITableOfContents | Table of contents. | +| plone.thumb_icon | plone.app.contenttypes.behaviors.thumb_icon.IThumbIconHandling | Adds options to suppress thumbs (preview images) or icons and to override thumb size in listings or tables. | diff --git a/plone.app.dexterity/reference/widgets.md b/plone.app.dexterity/reference/widgets.md new file mode 100644 index 000000000..5e83ca50a --- /dev/null +++ b/plone.app.dexterity/reference/widgets.md @@ -0,0 +1,32 @@ +--- +myst: + html_meta: + "description": "Standard and common third party widgets for content types in Plone" + "property=og:description": "Standard and common third party widgets for content types in Plone" + "property=og:title": "Standard and common third party widgets for content types in Plone" + "keywords": "Plone, standard, common, third party widgets, content types" +--- + +# Widgets + +This chapter describes the standard and common third party widgets for content types in Plone. + +Most of the time, you will use the standard widgets provided by `z3c.form`. +To learn more about `z3c.form` widgets, see the [z3c.form documentation](https://z3cform.readthedocs.io/en/latest/widgets/index.html). +To learn about setting custom widgets for Dexterity content types, see the {ref}`schema introduction `. + +The table below shows some commonly used custom widgets. + +| Widget | Imported from | Field | Description | +| ---------------------------- | ----------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| WysiwygFieldWidget | plone.app.z3cform.wysiwyg | Text | Use Plone's standard WYSIWYG HTML editor on a standard text field. Note that if you used a `RichText` field, you will get the WYSIWYG editor automatically. | +| RichTextWidget | plone.app.textfield.widget | RichText | Use Plone's standard WYSIWYG HTML editor on a `RichText` field. This also allows text-based markup such as reStructuredText. | +| AutocompleteFieldWidget | plone.formwidget.autocomplete | Choice | Autocomplete widget based on jQuery Autocomplete. Requires a Choice field with a query source. See {doc}`../advanced/vocabularies`. | +| AutocompleteMultiFieldWidget | plone.formwidget.autocomplete | Collection | Multi-select version of the above. Used for a List, Tuple, Set, or Frozenset with a Choice `value_type`. | +| ContentTreeFieldWidget | plone.formwidget.contenttree | RelationChoice | Content browser. Requires a query source with content objects as values. | +| MultiContentTreeFieldWidget | plone.formwidget.contenttree | RelationList | Content browser. Requires a query source with content objects as values. | +| NamedFileFieldWidget | plone.formwidget.namedfile | NamedFile/NamedBlobFile | A file upload widget | +| NamedImageFieldWidget | plone.formwidget.namedfile | NamedImage/NamedBlobImage | An image upload widget | +| TextLinesFieldWidget | plone.z3cform.textlines | Collection | One-per-line list entry for List, Tuple, Set, or Frozenset fields. Requires a `value_type` of TextLine or ASCIILine. | +| SingleCheckBoxFieldWidget | z3c.form.browser.checkbox | Bool | A single checkbox for `True` or `False`. | +| CheckBoxFieldWidget | z3c.form.browser.checkbox | Collection | A set of checkboxes. Used for Set or Frozenset fields with a Choice `value_type` and a vocabulary. | diff --git a/plone.app.dexterity/schema-driven-types.md b/plone.app.dexterity/schema-driven-types.md new file mode 100644 index 000000000..dcb5ca76b --- /dev/null +++ b/plone.app.dexterity/schema-driven-types.md @@ -0,0 +1,405 @@ +--- +myst: + html_meta: + "description": "Schema-driven types in Plone" + "property=og:description": "Schema-driven types in Plone" + "property=og:title": "Schema-driven types in Plone" + "keywords": "Plone, schema-driven, types" +--- + +# Schema-driven types + +This chapter describes how to create a minimal type based on a schema. + + +(the-schema-label)= + +## The schema + +A simple Dexterity type consists of a schema and a Factory Type Information (FTI), the object configured in {guilabel}`portal_types` in the ZMI. +We'll create the schemata here, and the FTI on the next page. + +Each schema is typically in a separate module. +Thus, we will add three files to our product: `presenter.py`, `program.py`, and `session.py`. +Each will start off with a schema interface. + + +## Creating base files + +`mr.bob` created some support for our initial content type, `Program`. +Let's clean it up a bit, then add another content type. + +```{note} +The template's original setup assumed we added a single content type. +In fact, we're going to add three. +We'll separate their support files in order to keep our code clean. +``` + +In your package's `src/example/conference` directory, you should find a file named `interfaces.py`. +Copy that file to `program.py` in the same directory. +In `interfaces.py`, delete the `IProgram` class. +In `program.py`, delete the `IExampleConferenceLayer` class. + +In your package's `src/example/conference/profiles/default/types` directory, you should find a file named `Program.xml`. +Find the line that reads: + +```xml +example.conference.interfaces.IProgram +``` + +and change it to read: + +```xml +example.conference.program.IProgram +``` + +That makes our setup profile point to our renamed schema file. + + +(adding-sessions-label)= + +## Adding sessions + +Now let's add another content type for our conference sessions. + +First return to the `program.py` file. +Copy it to `session.py`. +In `session.py`, rename the `IProgram` class to `ISessions`. + +Copy the `Program.xml` file in the `types/` subdirectory to `Session.xml`. + +Change `example.conference.program.IProgram` to `example.conference.program.ISession` in the new XML file. +Change `Program` anywhere it appears to `Session`. +Do the same for `program` and `session`. + +Find the `src/example/conference/profiles/types.xml` file, add a new object declaration: + +```xml + + + + +``` + +Repeat the {ref}`adding-sessions-label` steps for `presenter.py`, `IPresenter`, and `presenter.xml`. + + +### Setting the schema + +Start with `program.py`. +Add schema declarations for our `start`, `end`, and `details` fields. + +The top part of the file should look like the following. + +```python +from example.conference import _ +from plone.app.textfield import RichText +from plone.supermodel import model +from zope import schema + + +class IProgram(model.Schema): + + """A conference program. Programs can contain Sessions.""" + + title = schema.TextLine( + title=_('Program name'), + ) + + description = schema.Text( + title=_('Program summary'), + ) + + start = schema.Datetime( + title=_('Start date'), + required=False, + ) + + end = schema.Datetime( + title=_('End date'), + required=False, + ) + + details = RichText( + title=_('Details'), + description=_('Details about the program.'), + required=False, + ) +``` + +If you haven't developed for Plone before, take special note of the `from example.conference import MessageFactory as _` code. +This is to aid future internationalization of the package. +Every string that is presented to the user should be wrapped in `_()`, as shown with the titles and descriptions below. + +The `_` lives in the package root `__init__.py` file. + +```python +from zope.i18nmessageid import MessageFactory + +_ = MessageFactory('example.conference') +``` + +Notice how we use the package name as the translation domain. + +Notice how we use the field names `title` and `description` for the name and summary. +We do this to provide values for the default title and description metadata used in Plone's folder listings and searches, which defaults to these fields. +In general, every type should have a `title` field, although it could be provided by behaviors (more on those later). + +Save `program.py`. + +`session.py` for the Session type should look like the following. + +```python +from example.conference import _ +from plone.app.textfield import RichText +from plone.supermodel import model +from zope import schema + + +class ISession(model.Schema): + + """A conference session. Sessions are managed inside Programs.""" + + title = schema.TextLine( + title=_('Title'), + description=_('Session title'), + ) + + description = schema.Text( + title=_('Session summary'), + ) + + details = RichText( + title=_('Session details'), + required=False + ) +``` + +Note that we haven't added information about speakers or tracks yet. +We'll do that when we cover vocabularies and references later. + + +## Schema interfaces versus other interfaces + +As you may have noticed, each schema is basically just an interface (`zope.interface.Interface`) with fields. +The standard fields are found in the [`zope.schema`](https://pypi.org/project/zope.schema/) package. +You should look at its interfaces (`parts/omelette/zope/schema/interfaces.py`) to learn about the various schema fields available, and review [`zope.schema`'s documentation](https://zopeschema.readthedocs.io/en/latest/) for the package. +You may also want to look up [`plone.namedfile`](https://pypi.org/project/plone.namedfile/), which you can use if you require a file field, [`plone.app.relationfield`](https://pypi.org/project/plone.app.relationfield/), which can be used for references, and [`plone.app.textfield`](https://pypi.org/project/plone.app.textfield/), which supports rich text with a WYSIWYG editor. +We will cover these field types later in this manual. +They can also be found in the reference at the end. + +Unlike a standard interface, however, we are deriving from `model.Schema` (actually, `plone.supermodel.model.Schema`). +This is just a marker interface that allows us to add some form hints to the interface, which are then used by Dexterity (actually, the [`plone.autoform`](https://pypi.org/project/plone.autoform/) package) to construct forms. +Take a look at the [`plone.autoform` documentation](https://pypi.org/project/plone.autoform/#introduction) to learn more about the various hints that are possible. +The most common ones are from `plone.autoform.directives`. +Use `fieldset()` to define groups of fields, `widget()` to set widgets for particular fields and `omitted()` to hide one or more fields from the form. +We will see examples of these later in the manual. + + +(zope-schema)= + +## Factory Type Information (FTI) + +This section describes how to add a Factory Type Information (FTI) object for the type. + +When we created the files `types/session.xml` and `types/presenter.xml`, and added object declarations to `types.xml`, we made our new content types installable. +These XML configuration files are referred to as Generic Setup Profiles. + +Look in the `types.xml` file in your package's `example/conference/profiles/default` directory. + +```xml + + + + + +``` + +Note that the type name should be unique. +If it isn't, use the package name as a prefix and the type name to create a unique name. +It is important that the `meta_type` is `Dexterity FTI`. +The FTI specification is what makes this a Dexterity file type. +The `types/` file name must match the type's name. + +Let's take a look at a `types/` XML file. +The `Session` type, in `session.xml`, should look like the following. + +```xml + + + Session + Conference Session + + + string:${portal_url}/document_icon.png + + + Session + + + string:${folder_url}/++add++Session + + + view + + + False + + + True + + + + + False + + + view + + + + + False + + + cmf.AddPortalContent + + + plone.dexterity.content.Item + + + + + + + + example.conference.session.ISession + + + + + + + + + + + + + + + + + + + +``` + +Note that the `icon_expr` and `global_allow` declarations have changed from the original. + +There is a fair amount of boilerplate here which could actually be omitted, because the Dexterity FTI defaults will take care of most of this. +However, it is useful to see the options available so that you know what you can change. + +The important lines here are: + +- The `name` attribute on the root element must match the name in `types.xml` and the filename. +- We use the package name as the translation domain again, via `i18n:domain`. +- We set a title and description for the type +- We specify an icon. + Here, we use a standard icon from Plone's `plone_images` skin layer. + You'll learn more about static resources later. +- We set `global_allow` to `False`, since these objects should only be addable inside a `Program`. +- The schema interface is referenced by the `schema` property. +- The `klass` property designates the base class of the content type. + Use `plone.dexterity.content.Item` or `plone.dexterity.content.Container` for a basic Dexterity `Item` (non-container) or `Container` for a type that acts like a folder. + You may also use your own class declarations if you wish to add class members or methods. + These are usually derived from `Item` or `Container`. +- We specify the name of an add permission. + The default `cmf.AddPortalContent` should be used unless you configure a custom permission. + Custom permissions are convered later in this manual. +- We add a behavior. + Behaviors are reusable aspects providing semantics or schema fields. + Here we add the `INameFromTitle` behavior, which will give our content object a readable ID based on the `title` property. + We'll cover other behaviors later. + +The `Program`, in `program.xml`, looks like the following. + +```xml + + + + + + Program + Conference Program + string:${portal_url}/folder_icon.png + Program + True + + + True + + + + + + example.conference.program.IProgram + plone.dexterity.content.Container + + + + +``` + +We've edited this one a little from the boilplate. +The difference here is that we make this a `Container`, and filter the containable types (`filter_content_types` and `allowed_content_types`) to allow only `Sessions` to be added inside this folder. + + +## Testing the type + +This section describes how to start up Plone and test the type. +It also provides some trouble-shooting tips. + +With a schema and FTI for each type, and our `GenericSetup` profile registered in `configure.zcml`, we should be able to test our type. +Make sure that you have run buildout, and then start `./bin/instance fg` as normal. +Add a Plone site, and go to the {guilabel}`Add-ons` control panel. +You should see your package there, and be able to install it. + +Once installed, you should be able to add objects of the new content types. + +If Zope doesn't start up: + +- Look for error messages on the console, and make sure you start in the foreground with `./bin/instance fg`. + You could have a syntax error or a ZCML error. + +If you don't see your package in the {guilabel}`Add-ons` control panel: + +- Ensure that the package is either checked out by `mr.developer`, or that you have a `develop` line in `buildout.cfg` to load it as a development egg. + `develop = src/*` should suffice, but you can also add the package explicitly, for example with `develop = src/example.conference`. +- Ensure that the package is actually loaded as an egg. + It should be referenced in the `eggs` section under `[instance]`. +- You can check that the package is correctly configured in the buildout by looking at the generated `bin/instance` script (`bin\instance-script.py` on Windows). + There should be a line for your package in the list of eggs at the top of the file. +- Make sure that the package's ZCML is loaded. + You can do this by installing a ZCML slug (via the `zcml` option in the `[instance]` section of `buildout.cfg`), or by adding an `` line in another package's `configure.zcml`. + However, the easiest way with Plone 3.3 and later is to add the `z3c.autoinclude.plugin` entry point to `setup.py`. +- Ensure that you have added a `` stanza to `configure.zcml`. + +If the package fails to install in the {guilabel}`Add-ons` control panel: + +- Look for errors in the `error_log` at the root of the Plone site, in your console, or in your log files. +- Check the syntax and placement of the profile files. + Remember that you need a `types.xml` listing your types, and corresponding files in `types/*.xml`. + +If your forms do not look right, for example custom widgets are missing, then: + +- Make sure your schema derives from `model.Schema`. +- Remember that the directives require you to specify the correct field name, even if they are placed before or after the relevant field. diff --git a/plone.app.dexterity/testing/index.md b/plone.app.dexterity/testing/index.md new file mode 100644 index 000000000..08d96a73a --- /dev/null +++ b/plone.app.dexterity/testing/index.md @@ -0,0 +1,20 @@ +--- +myst: + html_meta: + "description": "How to write unit and integration tests for Plone content types" + "property=og:description": "How to write unit and integration tests for Plone content types" + "property=og:title": "How to write unit and integration tests for Plone content types" + "keywords": "Plone" +--- + +# Testing Dexterity types + +This part describes how to write unit and integration tests for Plone content types. + +```{toctree} +:maxdepth: 2 + +unit-tests +integration-tests +mock-testing +``` diff --git a/plone.app.dexterity/testing/integration-tests.md b/plone.app.dexterity/testing/integration-tests.md new file mode 100644 index 000000000..bd0c53b01 --- /dev/null +++ b/plone.app.dexterity/testing/integration-tests.md @@ -0,0 +1,335 @@ +--- +myst: + html_meta: + "description": "How to write integration tests with plone.app.testing for content types in Plone" + "property=og:description": "How to write integration tests with plone.app.testing for content types in Plone" + "property=og:title": "How to write integration tests with plone.app.testing for content types in Plone" + "keywords": "Plone, content types, integration tests" +--- + +# Integration tests + +This chapter describes how to write integration tests with [`plone.app.testing`](https://pypi.org/project/plone.app.testing/). + +We'll now add some integration tests for our type. +These should ensure that the package installs cleanly, and that our custom types are addable in the right places and have the right schemata, at the very least. + +To help manage test setup, we'll make use of the Zope test runner's concept of *layers*. +Layers allow common test setup (such as configuring a Plone site and installing a product) to take place once and be reused by multiple test cases. +Those test cases can still modify the environment, but their changes will be torn down and the environment reset to the layer's initial state between each test, facilitating test isolation. + +As the name implies, layers are, uh..., layered. +One layer can extend another. +If two test cases in the same test run use two different layers with a common ancestral layer, the ancestral layer is only set up and torn down once. + +`plone.app.testing` provides tools for writing integration and functional tests for code that runs on top of Plone, so we'll use it. + +In {file}`setup.py`, we will add the `extras_require` option as shown. + +```python +extras_require = { + "test": ["plone.app.testing"] +}, +``` + +```{note} +Don't forget to re-run buildout after making changes to {file}`setup.py`. +``` + +`plone.app.testing` includes a set of layers that set up fixtures containing a Plone site, intended for writing integration and functional tests. + +We need to create a custom fixture. +The usual pattern is to create a new layer class that has `PLONE_FIXTURE` as its default base, instantiating that as a separate "fixture" layer. +This layer is not to be used in tests directly, since it won't have test and transaction lifecycle management, but represents a shared fixture, potentially for both functional and integration testing. +It is also the point of extension for other layers that follow the same pattern. + +Once this fixture has been defined, "end-user" layers can be defined using the `IntegrationTesting` and `FunctionalTesting` classes. +We'll add this in a {file}`testing.py` file. + +```python +from plone.app.testing import PloneSandboxLayer +from plone.app.testing import PLONE_FIXTURE +from plone.app.testing import IntegrationTesting +from plone.app.testing import FunctionalTesting + +class Fixture(PloneSandboxLayer): + + defaultBases = (PLONE_FIXTURE,) + + def setUpZope(self, app, configurationContext): + # Load ZCML + import example.conference + self.loadZCML(package=example.conference) + + def setUpPloneSite(self, portal): + # Install the example.conference product + self.applyProfile(portal, "example.conference:default") + + +FIXTURE = Fixture() +INTEGRATION_TESTING = IntegrationTesting( + bases=(FIXTURE,), + name="example.conference:Integration", + ) +FUNCTIONAL_TESTING = FunctionalTesting( + bases=(FIXTURE,), + name="example.conference:Functional", + ) +``` + +This extends a base layer that sets up Plone, and adds some custom layer setup for our package, in this case installing the `example.conference` extension profile. +We could also perform additional setup here, such as creating some initial content or setting the default roles for the test run. +See the [`plone.app.testing`](https://pypi.org/project/plone.app.testing/#introduction) documentation for more details. + +To use the layer, we can create a new test case based on `unittest.TestCase` that uses our layer. +We'll add one to `test_program.py` first. +In the code snippet below, the unit test we created previously has been removed to conserve space. + +```python +import unittest + +from zope.component import createObject +from zope.component import queryUtility + +from plone.app.testing import TEST_USER_ID +from plone.app.testing import setRoles + +from plone.dexterity.interfaces import IDexterityFTI + +from example.conference.program import IProgram +from example.conference.testing import INTEGRATION_TESTING + +class TestProgramIntegration(unittest.TestCase): + + layer = INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.portal.invokeFactory("Folder", "test-folder") + setRoles(self.portal, TEST_USER_ID, ["Member"]) + self.folder = self.portal["test-folder"] + + def test_adding(self): + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] + self.assertTrue(IProgram.providedBy(p1)) + + def test_fti(self): + fti = queryUtility(IDexterityFTI, name="example.conference.program") + self.assertNotEquals(None, fti) + + def test_schema(self): + fti = queryUtility(IDexterityFTI, name="example.conference.program") + schema = fti.lookupSchema() + self.assertEqual(IProgram, schema) + + def test_factory(self): + fti = queryUtility(IDexterityFTI, name="example.conference.program") + factory = fti.factory + new_object = createObject(factory) + self.assertTrue(IProgram.providedBy(new_object)) + + def test_view(self): + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] + view = p1.restrictedTraverse("@@view") + sessions = view.sessions() + self.assertEqual(0, len(sessions)) + + def test_start_end_dates_indexed(self): + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] + p1.start = datetime.datetime(2009, 1, 1, 14, 01) + p1.end = datetime.datetime(2009, 1, 2, 15, 02) + p1.reindexObject() + + result = self.portal.portal_catalog(path="/".join(p1.getPhysicalPath())) + + self.assertEqual(1, len(result)) + self.assertEqual(result[0].start, DateTime("2009-01-01T14:01:00")) + self.assertEqual(result[0].end, DateTime("2009-01-02T15:02:00")) + + def test_tracks_indexed(self): + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] + p1.tracks = ["Track 1", "Track 2"] + p1.reindexObject() + + result = self.portal.portal_catalog(Subject="Track 2") + + self.assertEqual(1, len(result)) + self.assertEqual(result[0].getURL(), p1.absolute_url()) + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) +``` + +This illustrates a basic set of tests that make sense for most content types. +There are many more things we could test. +For example, we could test the add permissions more thoroughly, and we ought to test the `sessions()` method on the view with some actual content, but even this small set of integration tests tells us that our product has installed, that the content type is addable, that it has the right factory, and that instances of the type provide the right schema interface. + +There are some important things to note about this test case. + +- We extend `unittest.TestCase`, which means we have access to a full Plone integration test environment. + See the [testing tutorial](https://5.docs.plone.org/external/plone.testing/docs/index.html) for more details. +- We set the `layer` attribute to our custom layer. + This means that all tests in our test case will have the `example.conference:default` profile installed. +- We need to create a test user's member folder as `self.folder` because `plone.app.testing` takes a minimalist approach and no content is available by default. +- We test that the content is addable as a normal member in their member folder, since that is the default security context for the test. + Use `self.setRoles([‘Manager'])` to get the `Manager` role and `self.portal` to access the portal root. + We also test that the FTI is installed and can be located, and that both the FTI and instances of the type know about the correct type schema. +- We also test that the view can be looked up and has the correct methods. + We've not included a fully functional test using `zope.testbrowser` or any other front-end testing here. + If you require those, take a look at the testing tutorial. +- We also test that our custom indexers are working, by creating an appropriate object and searching for it. + Note that we need to reindex the object after we've modified it so that the catalog is up to date. +- The `defaultTestLoader` will find this test and load it, just as it found the `TestProgramUnit` test case. + +To run our tests, we can still do. + +```shell +./bin/test example.conference +``` + +You should now notice layers being set up and torn down. +Again, use the `-t` option to run a particular test case (or test method) only. + +The other tests are similar. +We have {file}`tests/test_session.py` to test the `Session` type. + +```python +import unittest2 as unittest + +from zope.component import createObject +from zope.component import queryUtility + +from plone.app.testing import TEST_USER_ID +from plone.app.testing import setRoles + +from plone.dexterity.interfaces import IDexterityFTI + +from example.conference.session import ISession +from example.conference.session import possible_tracks +from example.conference.testing import INTEGRATION_TESTING + +class TestSessionIntegration(unittest.TestCase): + + layer = INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.portal.invokeFactory("Folder", "test-folder") + setRoles(self.portal, TEST_USER_ID, ["Member"]) + self.folder = self.portal["test-folder"] + + def test_adding(self): + + # We can't add this directly + self.assertRaises(ValueError, self.folder.invokeFactory, "example.conference.session", "session1") + + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] + + p1.invokeFactory("example.conference.session", "session1") + s1 = p1["session1"] + self.assertTrue(ISession.providedBy(s1)) + + def test_fti(self): + fti = queryUtility(IDexterityFTI, name="example.conference.session") + self.assertNotEquals(None, fti) + + def test_schema(self): + fti = queryUtility(IDexterityFTI, name="example.conference.session") + schema = fti.lookupSchema() + self.assertEqual(ISession, schema) + + def test_factory(self): + fti = queryUtility(IDexterityFTI, name="example.conference.session") + factory = fti.factory + new_object = createObject(factory) + self.assertTrue(ISession.providedBy(new_object)) + + def test_tracks_vocabulary(self): + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] + p1.tracks = ["T1", "T2", "T3"] + + p1.invokeFactory("example.conference.session", "session1") + s1 = p1["session1"] + + vocab = possible_tracks(s1) + + self.assertEqual(["T1", "T2", "T3"], [t.value for t in vocab]) + self.assertEqual(["T1", "T2", "T3"], [t.token for t in vocab]) + + def test_catalog_index_metadata(self): + self.assertTrue("track" in self.portal.portal_catalog.indexes()) + self.assertTrue("track" in self.portal.portal_catalog.schema()) + + def test_workflow_installed(self): + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] + + p1.invokeFactory("example.conference.session", "session1") + s1 = p1["session1"] + + chain = self.portal.portal_workflow.getChainFor(s1) + self.assertEqual(("example.conference.session_workflow",), chain) + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) +``` + +Notice here how we test that the `Session` type cannot be added directly to a folder, and that it can be added inside a program. +We also add a test for the `possible_tracks()` vocabulary method, as well as tests for the installation of the `track` index and metadata column and the custom workflow. + +```python +import unittest2 as unittest + +from zope.component import createObject +from zope.component import queryUtility + +from plone.app.testing import TEST_USER_ID +from plone.app.testing import setRoles + +from plone.dexterity.interfaces import IDexterityFTI + +from example.conference.presenter import IPresenter +from example.conference.testing import INTEGRATION_TESTING + +class TestPresenterIntegration(unittest.TestCase): + + layer = INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.portal.invokeFactory("Folder", "test-folder") + setRoles(self.portal, TEST_USER_ID, ["Member"]) + self.folder = self.portal["test-folder"] + + def test_adding(self): + self.folder.invokeFactory("example.conference.presenter", "presenter1") + p1 = self.folder["presenter1"] + self.assertTrue(IPresenter.providedBy(p1)) + + def test_fti(self): + fti = queryUtility(IDexterityFTI, name="example.conference.presenter") + self.assertNotEquals(None, fti) + + def test_schema(self): + fti = queryUtility(IDexterityFTI, name="example.conference.presenter") + schema = fti.lookupSchema() + self.assertEqual(IPresenter, schema) + + def test_factory(self): + fti = queryUtility(IDexterityFTI, name="example.conference.presenter") + factory = fti.factory + new_object = createObject(factory) + self.assertTrue(IPresenter.providedBy(new_object)) + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) +``` diff --git a/plone.app.dexterity/testing/mock-testing.md b/plone.app.dexterity/testing/mock-testing.md new file mode 100644 index 000000000..533e25147 --- /dev/null +++ b/plone.app.dexterity/testing/mock-testing.md @@ -0,0 +1,204 @@ +--- +myst: + html_meta: + "description": "How to use a mock objects framework to write mock based tests of content types in Plone" + "property=og:description": "How to use a mock objects framework to write mock based tests of content types in Plone" + "property=og:title": "How to use a mock objects framework to write mock based tests of content types in Plone" + "keywords": "Plone, content types, tests, mock objects, framework" +--- + +# Mock testing + +This chapter describes how to use a mock objects framework to write mock based tests. + +Mock testing is a powerful approach to testing that lets you make assertions about how the code under test is interacting with other system modules. +It is often useful when the code you want to test is performing operations that cannot be easily asserted by looking at its return value. + +In our example product, we have an event handler. + +```python +def notifyUser(presenter, event): + acl_users = getToolByName(presenter, "acl_users") + mail_host = getToolByName(presenter, "MailHost") + portal_url = getToolByName(presenter, "portal_url") + + portal = portal_url.getPortalObject() + sender = portal.getProperty("email_from_address") + + if not sender: + return + + subject = "Is this you?" + message = "A presenter called %s was added here %s" % (presenter.title, presenter.absolute_url(),) + + matching_users = acl_users.searchUsers(fullname=presenter.title) + for user_info in matching_users: + email = user_info.get("email", None) + if email is not None: + mail_host.send(message, email, sender, subject) +``` + +If we want to test that this sends the right kind of email message, we'll need to somehow inspect what is passed to `send().` +The only way to do that is to replace the `MailHost` object that is acquired when `getToolByName(presenter, ‘MailHost')` is called, with something that performs that assertion for us. + +If we wanted to write an integration test, we could use `PloneTestCase` to execute this event handler by firing the event manually, and temporarily replace the `MailHost` object in the root of the test case portal (`self.portal`) with a dummy that raised an exception if the wrong value was passed. + +However, such integration tests can get pretty heavy handed, and sometimes it is difficult to ensure that it works in all cases. +In the approach outlined above, for example, we would miss cases where no mail was sent at all. + +Enter mock objects. +A mock object is a "test double" that knows how and when it ought to be called. +The typical approach is as follows. + +- Create a mock object. +- The mock object starts out in "record" mode. +- Record the operations that you expect the code under test perform on the mock object. + You can make assertions about the type and value of arguments, the sequence of calls, the number of times a method is called, or whether an attribute is retrieved or set. +- You can also give your mock objects behavior, such as specifying return values or exceptions to be raised in certain cases. +- Initialize the code under test or the environment it runs in so that it will use the mock object rather than the real object. + Sometimes this involves temporarily "patching" the environment. +- Put the mock framework into "replay" mode. +- Run the code under test. +- Apply any assertions as you normally would. +- The mock framework will raise exceptions if the mock objects are called incorrectly, such as with the wrong arguments or too many times, or insufficiently, such as an expected method was not called. + +There are several Python mock object frameworks. +Dexterity itself uses a powerful one called [`mocker`](https://labix.org/mocker), via the [`plone.mocktestcase`](https://pypi.org/project/plone.mocktestcase/) integration package. +You are encouraged to read the documentation for those two packages to better understand how mock testing works, and what options are available. + +```{note} +Take a look at the tests in `plone.dexterity` if you're looking for more examples of mock tests using `plone.mocktestcase`. +``` + +To use the mock testing framework, we first need to depend on `plone.mocktestcase`. +As usual, we add it to {file}`setup.py` and re-run buildout. + +```python +install_requires=[ + # ... + "plone.mocktestcase", +], +``` + +As an example test case, consider the following class in {file}`test_presenter.py`. + +```python +import unittest + +# ... + +from plone.mocktestcase import MockTestCase +from zope.app.container.contained import ObjectAddedEvent +from example.conference.presenter import notifyUser + +class TestPresenterUnit(MockTestCase): + + def test_notify_user(self): + + # dummy presenter + presenter = self.create_dummy( + __parent__=None, + __name__=None, + title="Jim", + absolute_url = lambda: "http://example.org/presenter", + ) + + # dummy event + event = ObjectAddedEvent(presenter) + + # search result for acl_users + user_info = [{"email": "jim@example.org", "id": "jim"}] + + # email data + message = "A presenter called Jim was added here http://example.org/presenter" + email = "jim@example.org" + sender = "test@example.org" + subject = "Is this you?" + + # mock tools/portal + + portal_mock = self.mocker.mock() + self.expect(portal_mock.getProperty("email_from_address")).result("test@example.org") + + portal_url_mock = self.mocker.mock() + self.mock_tool(portal_url_mock, "portal_url") + self.expect(portal_url_mock.getPortalObject()).result(portal_mock) + + acl_users_mock = self.mocker.mock() + self.mock_tool(acl_users_mock, "acl_users") + self.expect(acl_users_mock.searchUsers(fullname="Jim")).result(user_info) + + mail_host_mock = self.mocker.mock() + self.mock_tool(mail_host_mock, "MailHost") + self.expect(mail_host_mock.send(message, email, sender, subject)) + + + # put mock framework into replay mode + self.replay() + + # call the method under test + notifyUser(presenter, event) + + # we could make additional assertions here, e.g. if the function + # returned something. The mock framework will verify the assertions + # about expected call sequences. + +# ... + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) +``` + +Note that the other tests in this module have been removed for the sake of brevity. + +If you are not familiar with mock testing, it may take a bit of time to get your head around what's going on here. +Let's run though the test. + +- First, we create a dummy presenter object. + This is *not* a mock object, it's just a class with the required minimum set of attributes, created using the `create_dummy()` helper method from the `MockTestCase` base class. + We use this type of dummy because we are not interested in making any assertions on the `presenter` object: it is used as an "input" only. +- Next, we create a dummy event. + Here we have opted to use a standard implementation from `zope.app.container`. +- We then define a few variables that we will use in the various assertions and mock return values: the user data that will form our dummy user search results, and the email data passed to the mail host. +- Next, we create mocks for each of the tools that our code needs to look up. + For each, we use the `expect()` method from `MockTestCase` to make some assertions. + For example, we expect that `getPortalObject()` will be called (once) on the `portal_url` tool, and it should return another mock object, the `portal_mock`. + On this, we expect that `getProperty()` is called with an argument equal to `"email_from_address"`. + The mock will then return `"test@example.org"`. + Take a look at the `mocker` and `plone.mocktestcase` documentation to see the various other types of assertions you can make. +- The most important mock assertion is the line `self.expect(mail_host_mock.send(message, email, sender, subject))`. + This asserts that the `send()` method gets called with the required message, recipient address, sender address, and subject, exactly once. +- We then put the mock into replay mode, using `self.replay()`. + Up until this point, any calls on our mock objects have been to record expectations and specify behaviour. + From now on, any call will count towards verifying those expectations. +- Finally, we call the code under test with our dummy presenter and event. +- In this case, we don't have any "normal" assertions, although the usual unit test assertion methods are all available if you need them, for example, to test the return value of the method under test. + The assertions in this case are all coming from the mock objects. + The `tearDown()` method of the `MockTestCase` class will in fact check that all the various methods were called exactly as expected. + +To run these tests, use the normal test runner. + +```shell +./bin/test example.conference -t TestPresenterMock +``` + +Note that mock tests are typically as fast as unit tests, so there is typically no need for something like roadrunner. + + +## Mock testing caveats + +Mock testing is a somewhat controversial topic. +On the one hand, it allows you to write tests for things that are often difficult to test, and a mock framework can—once you are familiar with it—make child's play out of the often laborious task of creating reliable test doubles. +On the other hand, mock based tests are inevitably tied to the implementation of the code under test, and sometimes this coupling can be too tight for the test to be meaningful. +Using mock objects normally also means that you need a very good understanding of the external APIs you are mocking. +Otherwise, your mock may not be a good representation of how these systems would behave in the real world. +Much has been written on this, including [_Mocks Aren't Stubs_ by Martin Fowler](https://www.martinfowler.com/articles/mocksArentStubs.html). + +As always, it pays to be pragmatic. +If you find that you can't write a mock based test without reading every line of code in the method under test and reverse engineering it for the mocks, then an integration test may be more appropriate. +In fact, it is prudent to have at least some integration tests in any case, since you can never be 100% sure your mocks are valid representations of the real objects they are mocking. + +On the other hand, if the code you are testing is using well-defined APIs in a relatively predictable manner, mock objects can be a valuable way to test the "side effects" of your code, and a helpful tool to simulate things like exceptions and input values that may be difficult to produce otherwise. + +Remember also that mock objects are not necessarily an "all or nothing" proposition. +You can use simple dummy objects or "real" instances in most cases, and augment them with a few mock objects for those difficult-to-replicate test cases. diff --git a/plone.app.dexterity/testing/unit-tests.md b/plone.app.dexterity/testing/unit-tests.md new file mode 100644 index 000000000..df65876ab --- /dev/null +++ b/plone.app.dexterity/testing/unit-tests.md @@ -0,0 +1,144 @@ +--- +myst: + html_meta: + "description": "How to write basic unit tests for content types in Plone" + "property=og:description": "How to write basic unit tests for content types in Plone" + "property=og:title": "How to write basic unit tests for content types in Plone" + "keywords": "Plone, content types, unit tests" +--- + +# Unit tests + +This chapter describes how to write basic unit tests for content types. + +As all good developers know, automated tests are very important. +If you are not comfortable with automated testing and test-driven development, you should read the [Plone testing tutorial](https://5.docs.plone.org/external/plone.testing/docs/index.html). +In this section, we will assume you are familiar with Plone testing basics, and show some tests that are particularly relevant to our example types. + +Firstly, we will add a few unit tests. +Recall that unit tests are simple tests for a particular function or method, and do not depend on an outside environment being set up. +As a rule of thumb, if something can be tested with a simple unit test, do so for the following reasons. + +- Unit tests are quick to write. +- They are also quick to run. +- Because they are more isolated, you are less likely to have tests that pass or fail due to incorrect assumptions or by luck. +- You can usually test things more thoroughly and exhaustively with unit tests than with (slower) integration tests. + +You'll typically supplement a larger number of unit tests with a smaller number of integration tests, to ensure that your application's correctly wired up and working. + +That's the theory, at least. +When we write content types, we're often more interested in integration tests, because a type schema and FTI are more like configuration of the Plone and Dexterity frameworks than imperative programming. +We can't "unit test" the type's schema interface, but we can and should test that the correct schema is picked up and used when our type is installed. +We will often write unit tests (with mock objects, where required) for custom event handlers, default value calculation functions and other procedural code. + +In that spirit, let's write some unit tests for the default value handler and the invariant in {file}`program.py`. +We'll add the directory `tests`, with an `__init__.py` and a file {file}`test_program.py` as shown. + +```python +import unittest +import datetime + +from example.conference.program import startDefaultValue +from example.conference.program import endDefaultValue +from example.conference.program import IProgram +from example.conference.program import StartBeforeEnd + +class MockProgram(object): + pass + +class TestProgramUnit(unittest.TestCase): + """Unit test for the Program type + """ + + def test_start_defaults(self): + data = MockProgram() + default_value = startDefaultValue(data) + today = datetime.datetime.today() + delta = default_value - today + self.assertEqual(6, delta.days) + + def test_end_default(self): + data = MockProgram() + default_value = endDefaultValue(data) + today = datetime.datetime.today() + delta = default_value - today + self.assertEqual(9, delta.days) + + def test_validate_invariants_ok(self): + data = MockProgram() + data.start = datetime.datetime(2009, 1, 1) + data.end = datetime.datetime(2009, 1, 2) + + try: + IProgram.validateInvariants(data) + except: + self.fail() + + def test_validate_invariants_fail(self): + data = MockProgram() + data.start = datetime.datetime(2009, 1, 2) + data.end = datetime.datetime(2009, 1, 1) + + try: + IProgram.validateInvariants(data) + self.fail() + except StartBeforeEnd: + pass + + def test_validate_invariants_edge(self): + data = MockProgram() + data.start = datetime.datetime(2009, 1, 2) + data.end = datetime.datetime(2009, 1, 2) + + try: + IProgram.validateInvariants(data) + except: + self.fail() + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) +``` + +This is a test using the Python standard library's `unittest` module. +There are a few things to note here: + +- We have created a dummy class to simulate a `Program` instance. + It doesn't contain anything at all, but we set some attributes onto it for certain tests. + This is a very simple way to do mocks. + There are much more sophisticated mock testing approaches, but starting simple is good. +- Each test is self contained. + There is no test layer or test case setup or tear down. +- We use the `defaultTestLoader` to load all test classes in the module automatically. + The test runner will look for modules in the `tests` package with names starting with `test` that have a `test_suite()` method to get test suites. + +To run the tests, use the following command. + +```shell +./bin/test example.conference +``` + +Hopefully it should show five passing tests. + +```{note} +This uses the testrunner configured via the `[test]` part in our `buildout.cfg`. +This provides better test reporting and a few more advanced options, such as output coloring. +We could also use the built-in test runner in the `instance` script, for example, with `./bin/instance test -s example.conference`. +``` + +To run just this test suite, use the following command. + +```shell +./bin/test example.conference -t TestProgramUnit +``` + +This is useful when we have other test suites that we don't want to run when they are integration tests and require lengthy setup. + +To get a report about test coverage, we can run the following command. + +```shell +./bin/test example.conference --coverage +``` + +Test coverage reporting is important. +If you have a module with low test coverage, it means that your tests do not cover many of the code paths in those modules, and so are less useful for detecting bugs or guarding against future problems. +Aim for 100% coverage.