From 9af23e4d20a1f7282692985b442c87a1404f4924 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 24 Feb 2023 02:53:32 -0800 Subject: [PATCH 01/53] Prepare for plone.app.dexterity docs --- docs/backend/{content-types.md => content-types/index.md} | 0 docs/backend/index.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/backend/{content-types.md => content-types/index.md} (100%) 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 From 02d6c1d049ee7369bface70be3f2675557958118 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 24 Feb 2023 03:36:57 -0800 Subject: [PATCH 02/53] Document how to add external documentation for administrators of documentation --- docs/contributing/admins.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/contributing/admins.md b/docs/contributing/admins.md index 98d3c782e..32830f7df 100644 --- a/docs/contributing/admins.md +++ b/docs/contributing/admins.md @@ -15,6 +15,42 @@ 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 From 675ced26c046e762b2441996d8c8e6fe8e03704a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 27 Feb 2023 04:26:49 -0800 Subject: [PATCH 03/53] Update docs/contributing/admins.md Co-authored-by: Jens W. Klein --- docs/contributing/admins.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/contributing/admins.md b/docs/contributing/admins.md index 32830f7df..03a459bb5 100644 --- a/docs/contributing/admins.md +++ b/docs/contributing/admins.md @@ -33,7 +33,8 @@ We did this for `plone.app.dexterity` and several other projects. 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 `**/` + 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 From de0df2e813b9eb179a7b3c0db7a1f5bfdabe5b85 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 5 Mar 2023 23:12:34 -0800 Subject: [PATCH 04/53] Initial conversion of reStructuredText to MyST --- plone.app.dexterity | 1 + 1 file changed, 1 insertion(+) create mode 160000 plone.app.dexterity diff --git a/plone.app.dexterity b/plone.app.dexterity new file mode 160000 index 000000000..f57211d25 --- /dev/null +++ b/plone.app.dexterity @@ -0,0 +1 @@ +Subproject commit f57211d25af44b183f20e8917195a1e68fd323c2 From 9d08f32fa98fb525d0438b23237a6e8c9e061b1d Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 5 Mar 2023 23:16:01 -0800 Subject: [PATCH 05/53] Initial commit of plone.app.dexterity conversion of reStructuredText to MyST --- plone.app.dexterity | 1 - plone.app.dexterity/advanced/behaviours.md | 54 ++ .../advanced/catalog-indexing-strategies.md | 327 ++++++++++ .../advanced/custom-add-and-edit-forms.md | 224 +++++++ .../advanced/custom-content-classes.md | 92 +++ plone.app.dexterity/advanced/defaults.md | 76 +++ .../advanced/event-handlers.md | 131 ++++ .../advanced/files-and-images.md | 125 ++++ plone.app.dexterity/advanced/index.md | 32 + plone.app.dexterity/advanced/permissions.md | 206 +++++++ plone.app.dexterity/advanced/references.md | 185 ++++++ .../rich-text-markup-transformations.md | 227 +++++++ .../advanced/static-resources.md | 141 +++++ plone.app.dexterity/advanced/validators.md | 122 ++++ plone.app.dexterity/advanced/vocabularies.md | 366 +++++++++++ .../webdav-and-other-file-representations.md | 569 ++++++++++++++++++ plone.app.dexterity/advanced/workflow.md | 391 ++++++++++++ .../behaviors/behavior-basics.md | 34 ++ .../creating-and-registering-behaviors.md | 143 +++++ plone.app.dexterity/behaviors/index.md | 23 + plone.app.dexterity/behaviors/intro.md | 44 ++ .../behaviors/providing-marker-interfaces.md | 237 ++++++++ .../behaviors/schema-only-behaviors.md | 116 ++++ .../behaviors/testing-behaviors.md | 264 ++++++++ plone.app.dexterity/custom-views.md | 311 ++++++++++ plone.app.dexterity/designing.md | 42 ++ plone.app.dexterity/index.md | 34 ++ plone.app.dexterity/install.md | 29 + plone.app.dexterity/intro.md | 83 +++ plone.app.dexterity/model-driven-types.md | 115 ++++ plone.app.dexterity/prerequisite.md | 169 ++++++ .../reference/dexterity-xml.md | 344 +++++++++++ plone.app.dexterity/reference/fields.md | 131 ++++ .../reference/form-schema-hints.md | 159 +++++ plone.app.dexterity/reference/index.md | 24 + .../reference/manipulating-content-objects.md | 382 ++++++++++++ plone.app.dexterity/reference/misc.md | 30 + .../reference/standard-behaviours.md | 38 ++ plone.app.dexterity/reference/widgets.md | 37 ++ plone.app.dexterity/schema-driven-types.md | 393 ++++++++++++ plone.app.dexterity/testing/index.md | 20 + .../testing/integration-tests.md | 385 ++++++++++++ plone.app.dexterity/testing/mock-testing.md | 263 ++++++++ plone.app.dexterity/testing/unit-tests.md | 171 ++++++ 44 files changed, 7289 insertions(+), 1 deletion(-) delete mode 160000 plone.app.dexterity create mode 100644 plone.app.dexterity/advanced/behaviours.md create mode 100644 plone.app.dexterity/advanced/catalog-indexing-strategies.md create mode 100644 plone.app.dexterity/advanced/custom-add-and-edit-forms.md create mode 100644 plone.app.dexterity/advanced/custom-content-classes.md create mode 100644 plone.app.dexterity/advanced/defaults.md create mode 100644 plone.app.dexterity/advanced/event-handlers.md create mode 100644 plone.app.dexterity/advanced/files-and-images.md create mode 100644 plone.app.dexterity/advanced/index.md create mode 100644 plone.app.dexterity/advanced/permissions.md create mode 100644 plone.app.dexterity/advanced/references.md create mode 100644 plone.app.dexterity/advanced/rich-text-markup-transformations.md create mode 100644 plone.app.dexterity/advanced/static-resources.md create mode 100644 plone.app.dexterity/advanced/validators.md create mode 100644 plone.app.dexterity/advanced/vocabularies.md create mode 100644 plone.app.dexterity/advanced/webdav-and-other-file-representations.md create mode 100644 plone.app.dexterity/advanced/workflow.md create mode 100644 plone.app.dexterity/behaviors/behavior-basics.md create mode 100644 plone.app.dexterity/behaviors/creating-and-registering-behaviors.md create mode 100644 plone.app.dexterity/behaviors/index.md create mode 100644 plone.app.dexterity/behaviors/intro.md create mode 100644 plone.app.dexterity/behaviors/providing-marker-interfaces.md create mode 100644 plone.app.dexterity/behaviors/schema-only-behaviors.md create mode 100644 plone.app.dexterity/behaviors/testing-behaviors.md create mode 100644 plone.app.dexterity/custom-views.md create mode 100644 plone.app.dexterity/designing.md create mode 100644 plone.app.dexterity/index.md create mode 100644 plone.app.dexterity/install.md create mode 100644 plone.app.dexterity/intro.md create mode 100644 plone.app.dexterity/model-driven-types.md create mode 100644 plone.app.dexterity/prerequisite.md create mode 100644 plone.app.dexterity/reference/dexterity-xml.md create mode 100644 plone.app.dexterity/reference/fields.md create mode 100644 plone.app.dexterity/reference/form-schema-hints.md create mode 100644 plone.app.dexterity/reference/index.md create mode 100644 plone.app.dexterity/reference/manipulating-content-objects.md create mode 100644 plone.app.dexterity/reference/misc.md create mode 100644 plone.app.dexterity/reference/standard-behaviours.md create mode 100644 plone.app.dexterity/reference/widgets.md create mode 100644 plone.app.dexterity/schema-driven-types.md create mode 100644 plone.app.dexterity/testing/index.md create mode 100644 plone.app.dexterity/testing/integration-tests.md create mode 100644 plone.app.dexterity/testing/mock-testing.md create mode 100644 plone.app.dexterity/testing/unit-tests.md diff --git a/plone.app.dexterity b/plone.app.dexterity deleted file mode 160000 index f57211d25..000000000 --- a/plone.app.dexterity +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f57211d25af44b183f20e8917195a1e68fd323c2 diff --git a/plone.app.dexterity/advanced/behaviours.md b/plone.app.dexterity/advanced/behaviours.md new file mode 100644 index 000000000..a5bc2831f --- /dev/null +++ b/plone.app.dexterity/advanced/behaviours.md @@ -0,0 +1,54 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Using behaviors + +**Finding and adding behaviors** + +Dexterity introduces the concept of *behaviors* – re-usable bundles of +functionality and/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 in this manual, but we will show +how to enable behaviors on a type. Writing behaviors is covered in the +[Behaviors manual](http://docs.plone.org/external/plone.app.dexterity/docs/behaviors/index.html). + +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]. For a list +of standard behaviors that ship with Dexterity, see the reference at the +end of this manual. + +[plone.app.dexterity]: http://pypi.python.org/pypi/plone.app.dexterity 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..5b36d6722 --- /dev/null +++ b/plone.app.dexterity/advanced/catalog-indexing-strategies.md @@ -0,0 +1,327 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# 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. + +So, 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} +Note that 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 like 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 the [/backend/indexing](https://6-dev-docs.plone.org/backend/indexing) package documentation for details and for information on how to use it via Python schema. + +## Creating and using custom indexes + +**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 optimised 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} +Dexterity objects are more lightweight than Archetypes objects. +This means that loading objects into memory is not quite as undesirable as is sometimes assumed. +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: + +`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, the catalog will by default 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/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 (`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 +`profiles/default/catalog.xml`, we have: + +```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 can sometimes 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] to help make it easier to write custom indexers: +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 `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 finalise 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 are creating 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: + +```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 like `/foo/bar`. + To get the path of an object (e.g. a parent folder), do + `'/'.join(folder.getPhysicalPath())`. + Searching for an object’s path will return the object and any children. + To depth-limit the search, e.g. to get only those 1 level deep, + use a compound query, e.g. + `path={'query': '/'.join(folder.getPhysicalPath()), 'depth': 1}`. + If a 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 like `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 optimisations. + Use a list slice on the catalog search results to be absolutely sure that you have got the maximum number of results, e.g. + `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: + +*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]. + +#### How to setup the index TTW: + +Now that the fields are index-able, we need to create the index itself. + +- Go to the Zope Management Interface +- Go on 'portal_catalog' +- Click 'Indexes' tab +- There's a drop down menu to the top right to let you choose what type of index to add - if you are using a plain text string field you would select 'FieldIndex' +- As the 'id' put in the programmatical name of your Dexterity type field that you want to index +- Hit OK, tick your new index and click 'Reindex' + +You should now see content being indexed. + +See the {doc}`documentation ` for further information + +[plone.indexer]: http://pypi.python.org/pypi/plone.indexer +[zcatalog chapter in the zope 2 book]: https://zope.readthedocs.io/en/latest/zopebook/SearchingZCatalog.html 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..7e7df6908 --- /dev/null +++ b/plone.app.dexterity/advanced/custom-add-and-edit-forms.md @@ -0,0 +1,224 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Custom add and edit forms + +**Using \`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] library to build its forms, via the +[plone.z3cform] integration package. + +Dexterity also relies on [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. +::: + +## 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: + +```python +from plone.dexterity.browser import edit + +class EditForm(edit.DefaultEditForm): + pass +``` + +and register it in 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: + +- 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 + different 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] and [z3c.form] documentation + 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 realise 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 re-use 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 this: + +- 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 like `/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 initialises 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 initialised 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 look like this: + +```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 like this: + +```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 +(i.e. 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 to 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 customise this form by overriding [z3c.form] +and [plone.autoform] properties and methods. +See the [z3c.form] documentation on add forms for more details. + +[plone.autoform]: http://pypi.python.org/pypi/plone.autoform +[plone.z3cform]: http://pypi.python.org/pypi/plone.z3cform +[z3c.form]: http://pythonhosted.org/z3c.form 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..95edb1074 --- /dev/null +++ b/plone.app.dexterity/advanced/custom-content-classes.md @@ -0,0 +1,92 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Custom content classes + +**Adding a custom 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] 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. + +Creating a custom class is simple: simply derive from one of the +standard ones, e.g.: + +``` +from plone.dexterity.content import Item + +class MyItem(Item): + """A custom content class""" + ... +``` + +For a container type, we’d do: + +``` +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, e.g. + +```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]). +However, you need to be careful that this factory performs all necessary +initialisation, 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__` and/or + `isPrincipiaFolderish` properties, and possibly the `__getattr__()` + and `__getitem__()` methods, + causing problems with the dynamic schemata and/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. + +[plone.dexterity.content]: http://pypi.python.org/pypi/plone.dexterity.content +[plone.dexterity.factory]: http://pypi.python.org/pypi/plone.dexterity.factory diff --git a/plone.app.dexterity/advanced/defaults.md b/plone.app.dexterity/advanced/defaults.md new file mode 100644 index 000000000..7d8d8f556 --- /dev/null +++ b/plone.app.dexterity/advanced/defaults.md @@ -0,0 +1,76 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Defaults + +**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 +`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=_(u"Start date"), + required=False, + defaultFactory=startDefaultValue, + ) + + end = schema.Datetime( + title=_(u"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] documentation for more details. + +We’ll cover creating custom forms later in this manual. + +[z3c.form]: https://pypi.python.org/pypi/z3c.form#look-up-value-from-default-adapter diff --git a/plone.app.dexterity/advanced/event-handlers.md b/plone.app.dexterity/advanced/event-handlers.md new file mode 100644 index 000000000..3d0e2a2c9 --- /dev/null +++ b/plone.app.dexterity/advanced/event-handlers.md @@ -0,0 +1,131 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Event handlers + +**Adding 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] package) all registered event handlers will be called. +This happens for example from the `save` action of an add form, on move or delete of content-objects. +There is no guarantee of which order the event handlers will be called in, however. + +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 send these users an email. + +First, we require an additional import at the top of `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: + +- First argument to `for` is an interface describing the object type. +- 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. + +[zope.event]: http://pypi.python.org/pypi/zope.event 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..12ef41677 --- /dev/null +++ b/plone.app.dexterity/advanced/files-and-images.md @@ -0,0 +1,125 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Files and images + +**Working 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] and +[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 +like `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 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 +``. +::: + +For the sake of illustration, we will add an image of the +speaker to the `Presenter` type. In `presenter.py`, we add: + +``` +from plone.namedfile.field import NamedBlobImage + +class IPresenter(model.Schema): + ... + + picture = NamedBlobImage( + title=_(u"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 `presenter_templates/view.pt`, we add this block of TAL: + +``` +
+ +
+``` + +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: + +``` +
+ +
+``` + +where `scales` is large, preview, mini, thumb, tile, icon, or a custom scale. +This code generates a full tag, including height and width attributes and alt and title based on the context title. +To generate just a URL, use code like: + +``` + +``` + +For file fields, you can construct a download URL in a similar way, +using an `` tag, e.g.: + +``` + +``` + +[extra]: http://peak.telecommunity.com/DevCenter/setuptools#declaring-extras-optional-features-with-their-own-dependencies +[plone.formwidget.namedfile]: http://pypi.python.org/pypi/plone.formwidget.namedfile +[plone.namedfile]: http://pypi.python.org/pypi/plone.namedfile +[z3c.blobfile]: http://pypi.python.org/pypi/z3c.blobfile diff --git a/plone.app.dexterity/advanced/index.md b/plone.app.dexterity/advanced/index.md new file mode 100644 index 000000000..885fda59e --- /dev/null +++ b/plone.app.dexterity/advanced/index.md @@ -0,0 +1,32 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Advanced configuration + +**Further configuration and tips** + +```{toctree} +:maxdepth: 2 + +defaults.rst +validators.rst +vocabularies.rst +references.rst +rich-text-markup-transformations.rst +files-and-images.rst +static-resources.rst +behaviours.rst +event-handlers.rst +permissions.rst +workflow.rst +catalog-indexing-strategies.rst +custom-add-and-edit-forms.rst +custom-content-classes.rst +webdav-and-other-file-representations.rst +``` diff --git a/plone.app.dexterity/advanced/permissions.md b/plone.app.dexterity/advanced/permissions.md new file mode 100644 index 000000000..8c3d9c8cb --- /dev/null +++ b/plone.app.dexterity/advanced/permissions.md @@ -0,0 +1,206 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Permissions + +**Setting up add permissions, view permissions and field view/edit permissions** + +Plone’s security system is based on the concept of +*permissions* protecting *operations* +(like accessing a view, +viewing a field, +modifying a field, +or adding a type of content) +that are granted to *roles*, +which in turn are granted to *users* and/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] 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 `session.py`, we update the `View` class with the following: + +``` +from zope.security import checkPermission + +class View(BrowserView): + + def canRequestReview(self): + return checkPermission('cmf.RequestReview', self.context) +``` + +And in the `session_templates/view.pt` template, we add: + +```html +
+ 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 +`profiles/default/example.conference.session.xml`: + +```xml +example.conference.AddSession +``` + +## Protecting views and forms + +Access to views and other browser resources (like viewlets or portlets) +can be protected by permissions, either using the `permission` attribute +on ZCML statements like: + +``` + +``` + +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 +(e.g. scripts or templates created through the web) +and URL traversal, +and 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] package. + +If XML-schemas are used for defintion see {ref}`Dexterity XML: security attributes `. + +Simple example protecting a field to be readable for Site Administrators only: + +``` +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=_(u"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 `program.py`: + +``` +form.widget(tracks=TextLinesFieldWidget) +tracks = schema.List( + title=_(u"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: + +``` +from plone.z3cform.textlines.textlines import TextLinesFieldWidget +``` + +Next, we’ll add a vocabulary for this to `session.py`: + +``` +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) +and 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: + +``` +write_permission(track='example.conference.ModifyTrack') +track = schema.Choice( + title=_(u"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. + +[plone.autoform]: http://pypi.python.org/pypi/plone.autoform +[security section]: http://docs.plone.org/develop/plone/security/index.html diff --git a/plone.app.dexterity/advanced/references.md b/plone.app.dexterity/advanced/references.md new file mode 100644 index 000000000..6e3ce8392 --- /dev/null +++ b/plone.app.dexterity/advanced/references.md @@ -0,0 +1,185 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# References + +**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], a Zope +2 integration layer for [zope.intid], to give each content item a unique +integer id. These are the basis for relationships maintained with the +[zc.relationship] package, which in turn is accessed via an API +provided by [z3c.relationfield], integrated into Zope 2 with +[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] 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 `session.py`: + +``` +... + +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=_(u"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` like: + +``` +relatedItems = RelationList( + title=u"Related Items", + default=[], + value_type=RelationChoice(title=_(u"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 (e.g. 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 some that `plone.formwidget.contenttree` makes for you. For example: + +``` + + Related Items + + Related + plone.formwidget.contenttree.obj_path_src_binder + + +``` + +:::{note} +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 like +the autocomplete widget. The following line added to the interface will +make the presenter selection similar to the `organizer` selection widget +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 `templates/sessionview.pt`: + +```html +
+ + +
+``` + +## Back references + +To retrieve back-reference (all objects pointing to particular object using specified attribute) you can't simply use `from_object` or `from_path`, because source object is stored in the relation without acquisition wrappers. +You should use `from_id` and `helper` method, which search 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 +``` + +Please note, this method does not check effective and expiration date or content language. + +Original issue: [http://code.google.com/p/dexterity/issues/detail?id=234](http://code.google.com/p/dexterity/issues/detail?id=234) + +[five.intid]: http://pypi.python.org/pypi/five.intid +[plone.app.relationfield]: http://pypi.python.org/pypi/plone.app.relationfield +[plone.formwidget.contenttree]: http://pypi.python.org/pypi/plone.formwidget.contenttree +[z3c.relationfield]: http://pypi.python.org/pypi/z3c.relationfield +[zc.relationship]: http://pypi.python.org/pypi/zc.relationship +[zope.intid]: http://pypi.python.org/pypi/zope.intid 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..fbae0beb0 --- /dev/null +++ b/plone.app.dexterity/advanced/rich-text-markup-transformations.md @@ -0,0 +1,227 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Rich text, markup and transformations + +**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 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 this: + +``` +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=u"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] package: + +``` +from plone.app.textfield import RichText +from plone.supermodel import model + +class ITestSchema(model.Schema): + + body = RichText(title=u"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. + +Also note: 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 allow StructuredText and +reStructuredText, transformed to HTML by default: + +``` +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=u"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, e.g. `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 optimised for the case where +the transformed output will be read frequently (i.e. on the view screen +of the content object) and the raw value will be read infrequently (i.e. +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 should 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.): + +``` +from plone.app.textfield.value import RichTextValue +... + +context.body = RichTextValue(u"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 are using a +`DisplayForm`, the display widget for the `RichText` field will render +the transformed output markup automatically. If you are writing TAL +manually, you may try something like this: + +```html +
+``` + +This, however, will render a string like: + +``` +RichTextValue object. (Did you mean .raw or .output?) +``` + +The correct syntax is: + +```html +
+``` + +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. (Should you need to write your own transforms, +take a look at [this tutorial].) This is abstracted behind an +`ITransformer` adapter to allow alternative implementations. + +To invoke a transformation in code, you can use the following syntax: + +``` +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 are writing a page template, there is an even more convenient +syntax: + +```html +
+``` + +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. +::: + +[plone.app.textfield]: http://pypi.python.org/pypi/plone.app.textfield +[this tutorial]: http://plone.org/documentation/kb/portal-transforms diff --git a/plone.app.dexterity/advanced/static-resources.md b/plone.app.dexterity/advanced/static-resources.md new file mode 100644 index 000000000..0ce62cedf --- /dev/null +++ b/plone.app.dexterity/advanced/static-resources.md @@ -0,0 +1,141 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Static resources + +**Adding images and stylesheets** + +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 `conference.css`, it will be accessible on a URL +like `http:///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 +`presenter_templates/view.pt`: + +```html + + + + + +``` + +:::{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 +`cssregistry.xml` or `jsregistry.xml` import steps in the +`profiles/default` directory. For example, an import step to add the +`conference.css` file site-wide may involve a `cssregistry.xml` file +that looks like this: + +```xml + + + + +``` + +Similarly, a JavaScript resource could be imported with a +`jsregistry.xml` like: + +```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 +like this: + +```html + +``` + +## 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 +`profiles/default/types/example.conference.presenter.xml`, we can use +the following line, presuming we have a `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..8ecf195ed --- /dev/null +++ b/plone.app.dexterity/advanced/validators.md @@ -0,0 +1,122 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Validators + +**Creating 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] library +ensures that all data entered on 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] package for details, but +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 of the field’s type. The function +should return a boolean `True` or `False`. + +``` +def checkForMagic(value): + return 'magic' in value +``` + +:::{note} +Hint: 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, e.g.: + +``` +my_field = schema.TextLine(title=_(u"My field"), constraint=checkForMagic) +``` + +Constraints are easy to write, but do not necessarily produce very +friendly error messages. It is however possible to customise these error +messages using [z3c.form] error view snippets. See the [z3c.form +documentation](z3c.form) 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. +Code not relevant to this example is snipped with an ellipsis (…): + +``` +... + +from zope.interface import invariant, Invalid + +class StartBeforeEnd(Invalid): + __doc__ = _(u"The start or end date is invalid") + +class IProgram(model.Schema): + + ... + + start = schema.Datetime( + title=_(u"Start date"), + required=False, + ) + + end = schema.Datetime( + title=_(u"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(_(u"The start date must be before the end date.")) + +... +``` + +## Form validators + +Finally, you can write more powerful validators by using the [z3c.form](http://pypi.python.org/pypi/z3c.form) +widget validators. For details see the [z3c.form validators section](http://docs.plone.org/develop/plone/forms/z3c.form.html#validators). + +[zope.schema]: http://pypi.python.org/pypi/zope.schema diff --git a/plone.app.dexterity/advanced/vocabularies.md b/plone.app.dexterity/advanced/vocabularies.md new file mode 100644 index 000000000..d29b8f714 --- /dev/null +++ b/plone.app.dexterity/advanced/vocabularies.md @@ -0,0 +1,366 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Vocabularies + +**Creating your own static and dynamic vocabularies** + +Vocabularies are normally used in conjunction with selection fields, +and are supported by the [zope.schema] package, +with widgets provided by [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 organiser responsible for the program. + +## Static vocabularies + +Our first attempt uses a static list of organisers. +We use the message factory to allow the labels (term titles) 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=u'Bill', title=_(u'Bill')), + SimpleTerm(value=u'Bob', title=_(u'Bob')), + SimpleTerm(value=u'Jim', title=_(u'Jim')) + ] +) + +organizer = schema.Choice( + title=_(u"Organiser"), + 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 simply 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 +(i.e. 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 *tokenised* 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 tokenised 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 tokenised + vocabulary with the token and value specified. + +You can also instantiate a `SimpleVocabulary` yourself and pass a list +of terms in the initialiser. +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=_(u"Organiser"), + source=possibleOrganizers, + required=False, +) +``` + +## Parameterised 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 initialised 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=_(u"Organiser"), + source=GroupMembers('organizers'), + required=False, +) +``` + +When the schema is initialised on startup, a `GroupMembers` object +is instantiated, storing the desired group name. Each time the +vocabulary is needed, this object will be called (i.e. 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 re-usable, 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 parameterised 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 `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) +``` + +The add to your `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=_(u"Organiser"), + vocabulary=u"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] package. +A resent and complete list can be found in the README of the package. + +For our example we could use `plone.app.vocabularies.Users`, +that lists the users of the portal. + +The `organizer` field now looks like: + +```python +organizer = schema.Choice( + title=_(u"Organiser"), + vocabulary=u"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]. +But, in a Plone context, you will more likely want to use [plone.formwidget.autocomplete], +which extends `z3c.formwidget.query` to provide friendlier user interface. + +The widget is provided with [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 `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` `value_type`, +we would use the `AutocompleteMultiFieldWidget` instead. +::: + +In the `IProgram` schema (which, recall, 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=_(u'Organiser'), + vocabulary=u'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 fall-back for non-JavaScript capable browsers. + +[plone.app.dexterity]: http://pypi.python.org/pypi/plone.app.dexterity +[plone.app.vocabularies]: http://pypi.python.org/pypi/plone.app.vocabularies +[plone.formwidget.autocomplete]: http://pypi.python.org/pypi/plone.formwidget.autocomplete +[plone.principalsource]: http://pypi.python.org/pypi/plone.principalsource +[z3c.form]: http://pypi.python.org/pypi/z3c.form +[z3c.formwidget.query]: http://pypi.python.org/pypi/z3c.formwidget.query +[zope.schema]: http://pypi.python.org/pypi/zope.schema 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..ee574a0e5 --- /dev/null +++ b/plone.app.dexterity/advanced/webdav-and-other-file-representations.md @@ -0,0 +1,569 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# WebDAV and other file representations + +**Adding 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. 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] 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 such as Novell NetDrive. + +[^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 Mac OS X, the Finder claims to support WebDAV, but the implementation +is so flakey that it is just as likely to crash Mac OS X as it is to let +you browse files and folders. Use a dedicated WebDAV client instead, +such as [Cyberduck]. + +## Default WebDAV behaviour + +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]. For 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=_(u"Title"), + description=_(u"Session title"), + ) + + description = schema.Text( + title=_(u"Session summary"), + ) + + directives.primary('details') + details = RichText( + title=_(u"Session details"), + required=False + ) + + form.widget(presenter=AutocompleteFieldWidget) + presenter = RelationChoice( + title=_(u"Presenter"), + source=ObjPathSourceBinder(object_provides=IPresenter.__identifier__), + required=False, + ) + + form.write_permission(track='example.conference.ModifyTrack') + track = schema.Choice( + title=_(u"Track"), + source=possibleTracks, + required=False, + ) +``` + +This will actually apply the `IPrimaryField` marker interface from the +[plone.rfc822] package to the given field(s). + +A WebDAV download of this content item will by default look like this: + +``` +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. +In order 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. + +## Customising WebDAV behaviour + +There are several ways in which you can influence the WebDAV behaviour +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 serialisations and parsers for fields. + See the [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 + (e.g. an image editor if the MIME type is `image/jpeg`). + The file `plone.dexterity.filerepresentation` 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] 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 `program.py`, looks like this: + +``` +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 configure.zcml: + +``` + +``` + +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`. + +Here is a simple automated integration test for the same component: + +``` +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 customise or debug WebDAV behaviour. + +### 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] 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 behaviours will be turned into +UTF-8 encoded headers. +The primary field, if any, will be returned in the body, also most likely +encoded as an UTF-8 encoded string. +Binary data may be base64-encoded instead. + +A type which wishes to override this behaviour 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 +behaviours, and similarly read the body and update the corresponding primary +field. + +`NullResource.PUT()` is responsible for creating a new content object +and initialising 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 (e.g. 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 initialised 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 initialised. + +### `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; and +`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__`, so e.g. +reading the `creationdate` property calls `dav__creationdate()` on the +property sheet instance. +These methods in turn return values based on the property manager instance +(i.e. the content object). +In particular: + +`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 +(i.e. 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, +i.e. 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 simply 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] 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. + +[cyberduck]: http://cyberduck.ch/ +[external editor]: ../../../../../external-editor +[plone.dexterity.filerepresentation]: http://pypi.python.org/pypi/plone.dexterity.filerepresentation +[plone.locking]: http://pypi.python.org/pypi/plone.locking +[plone.recipe.zope2instance]: http://pypi.python.org/pypi/plone.recipe.zope2instance +[plone.rfc822]: http://pypi.python.org/pypi/plone.rfc822 +[plone.supermodel]: http://pypi.python.org/pypi/plone.supermodel +[zope.filerepresentation]: http://pypi.python.org/pypi/zope.filerepresentation diff --git a/plone.app.dexterity/advanced/workflow.md b/plone.app.dexterity/advanced/workflow.md new file mode 100644 index 000000000..ab17f1e46 --- /dev/null +++ b/plone.app.dexterity/advanced/workflow.md @@ -0,0 +1,391 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Workflow + +**Controlling 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. + +:::{note} +There is nothing Dexterity-specific in this section. +Everything here applies equally well to content objects +created with Archetypes or using CMF directly. +::: + +## A DCWorkflow refresher + +What follows is a fairly detailed description of [DCWorkflow], +originally posted [here]. +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 +like {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] 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, +e.g. 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 anonymous!). + +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 +(i.e. before the object enters the target state) +or just after it has been completed (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](http://docs.zope.org/zope2/zope2book/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 specialisation of `IReadSequence`, i.e. a tuple). +The default obviously looks at the mappings in the `portal_workflow` tool, +but it is possible to override the mapping, +e.g. 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 UI 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] 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 `setup.py`, we have: + +``` +install_requires=[ + ... + 'collective.wtf', +], +``` + +:::{note} +As before, the `` line in `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 `workflows.xml` file in `profiles/default` +which maps types to workflows. + +The workflow mapping in `profiles/default/workflows.xml` looks like +this: + +```xml + + + + + + + + +``` + +The CSV file itself is found in +`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]. 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] as a +starting point. +::: + +``` +"[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. + +[collective.wtf]: http://pypi.python.org/pypi/collective.wtf +[dcworkflow]: http://pypi.python.org/pypi/Products.DCWorkflow +[example.conference source code]: http://svn.plone.org/svn/collective/example.conference/trunk/example/conference/profiles/default/workflow_csv +[here]: http://www.martinaspeli.net/articles/dcworkflows-hidden-gems +[tales]: http://docs.zope.org/zope2/zope2book/AppendixC.html#tales-overview +[this template]: ../Workflow%20template.ods diff --git a/plone.app.dexterity/behaviors/behavior-basics.md b/plone.app.dexterity/behaviors/behavior-basics.md new file mode 100644 index 000000000..4cf8ca0fc --- /dev/null +++ b/plone.app.dexterity/behaviors/behavior-basics.md @@ -0,0 +1,34 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Behavior basics + +**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 consist 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..d85334e9c --- /dev/null +++ b/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md @@ -0,0 +1,143 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Creating and registering behaviors + +**How to create a basic behavior that provides form fields** + +The following example is based on the [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 *setup.py*: + +```python +install_requires=[ + ..., + 'plone.behavior', + 'zope.schema', + 'zope.interface', + 'zope.component', +], +``` + +Next, we have *behaviors.zcml*, which is included from *configure.zcml* and contains all necessary configuration to set up the behaviors. +It looks like this: + +```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 *behaviors.py* module looks like this: + +```python +"""Behaviours to assign tags (to ideas). + +Includes a form field and a behaviour 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 like this: +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=_(u'Categorization'), + fields=('tags',), + ) + + tags = Tags( + title=_(u"Tags"), + description=_(u"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 [Dexterity Developer Manual] 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. + +[collective.gtags]: http://svn.plone.org/svn/collective/collective.gtags +[dexterity developer manual]: ../index.html diff --git a/plone.app.dexterity/behaviors/index.md b/plone.app.dexterity/behaviors/index.md new file mode 100644 index 000000000..cdb466434 --- /dev/null +++ b/plone.app.dexterity/behaviors/index.md @@ -0,0 +1,23 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Behaviors + +**How to create re-usable behaviors for Dexterity types** + +```{toctree} +:maxdepth: 2 + +intro.rst +behavior-basics.rst +creating-and-registering-behaviors.rst +providing-marker-interfaces.rst +schema-only-behaviors.rst +testing-behaviors.rst +``` diff --git a/plone.app.dexterity/behaviors/intro.md b/plone.app.dexterity/behaviors/intro.md new file mode 100644 index 000000000..ffb6ecbc6 --- /dev/null +++ b/plone.app.dexterity/behaviors/intro.md @@ -0,0 +1,44 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Introduction + +**About this manual** + +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 re-usable 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 and/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 (e.g. adapters, event handlers, views, viwelets) once and re-use them easily. +- A more experienced developer is making functionality available for re-use by less experienced integrators. + For example, a behavior can be packaged up and release 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 [Dexterity Developer Manual] for more information about how to enable behaviors on a type and for a list of standard behaviors. + +To learn more about how behaviors in detail are implemented, see the [plone.behavior] package. + +[dexterity developer manual]: ../index.html +[plone.behavior]: http://pypi.python.org/pypi/plone.behavior 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..c6a271b91 --- /dev/null +++ b/plone.app.dexterity/behaviors/providing-marker-interfaces.md @@ -0,0 +1,237 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Providing marker interfaces + +**How to use behaviors to set marker interfaces on instances of a given 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. +E.g. to use a particular extension point provided by another component. +In this case, it may 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*. +For example: + +```xml + +``` + +One could imagine a viewlet based on [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 (e.g. 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 + +- an *ILocalRoleProvider* adapter to automatically grant local roles to the chosen reviewers, +- a custom indexer that lists the reviewers. + +The ZCML registration looks like this: + +```xml + +``` + +Notice the use of the *AnnotationStorage* factory. +This is a re-usable factory that can be used to easily create behaviors from schema interfaces that store their values in annotations. +We’ll describe this in more detail later. +We could just as easily have provided our own factory in this example. + +The *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=_(u'Ownership'), + fields=( + 'official_reviewers', + 'unofficial_reviewers' + ), + ) + + directives.widget(official_reviewers=AutocompleteMultiFieldWidget) + directives.write_permission(official_reviewers='iz.EditOfficialReviewers') + official_reviewers = schema.Tuple( + title=_(u'Official reviewers'), + description=_( + u'People or groups who may review this item in an official ' + u'capacity.' + ), + value_type=schema.Choice( + title=_(u"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=_(u'Unofficial reviewers'), + description=_( + u'People or groups who may review this item in a supplementary ' + u'capacity' + ), + value_type=schema.Choice( + title=_(u"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 *configure.zcml*: + +```xml + + +``` + +This is quite a 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 simply 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 re-usable behaviors, which can then be enabled on a per-type basis by integrators (or the same developers in lazier moments). + +[plone.pony]: http://pypi.python.org/pypi/plone.pony 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..cd1999a9c --- /dev/null +++ b/plone.app.dexterity/behaviors/schema-only-behaviors.md @@ -0,0 +1,116 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Schema-only behaviors using annotations or attributes + +**Writing 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 to 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] 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 means you can simply 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 simply 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 normal. + +## 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: +simply 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 *IRelatedItems* schema looks like this: + +```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=u"Categorization", + fields=['relatedItems']) + + relatedItems = RelationList( + title=u"Related Items", + default=[], + value_type=RelationChoice(title=u"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. + Background: 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 BLOBs 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 +::: + +[zope.annotation]: http://pypi.python.org/pypi/zope.annotation diff --git a/plone.app.dexterity/behaviors/testing-behaviors.md b/plone.app.dexterity/behaviors/testing-behaviors.md new file mode 100644 index 000000000..ec545e4e6 --- /dev/null +++ b/plone.app.dexterity/behaviors/testing-behaviors.md @@ -0,0 +1,264 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Testing behaviors + +**How to write unit tests for behaviors** + +Behaviors, like any other code, should be tested. +If you are writing 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. + +To learn more about the `Tags` field and how it works, see `tagging.rst`. + +Test setup +---------- + +Before we can run these tests, we need to load the collective.gtags +configuration. This will configure the behavior. + + >>> 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. + + >>> 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. + + >>> 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. + + >>> 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. + + >>> 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: + + >>> 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 simply 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 *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. In *tests.py*, we have: + +```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 *behaviors.rst* doctest from the same directory as the *tests.py* file. +To run the test, we can use the usual test runner: + +``` +$ ./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. +Here is the code: + +```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..1ce6cd1b4 --- /dev/null +++ b/plone.app.dexterity/custom-views.md @@ -0,0 +1,311 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Custom views + +**Configuring custom views and using display forms** + +## Simple views + +**Creating basic views** + +So far, our types have used the default views. +They use the *display* widgets from [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 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 `` `configure.zcml `` file: + +```xml + + + ... + + + + +``` + +Secondly 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 `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 (e.g. `text/html`) and the rendered output text. +*RichText* fields are covered in more detail {ref}`later in this manual `. +::: + +The view for `Presenter` is even simpler: + +```xml + + + ... + + + + +``` + +The template, in `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 realise that you can create any type of view using this technique. +Your view does not have to be related to a particular content type, even. +You could set the context to `Interface`, for example, to make a view that’s available on all types. + +## Display view + +**Using 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 re-use more complex HTML. + +To do this, you can use a *display view*. +This is really just a view base class that knows about the schema of a type. +We will use an example in `session.py`, with a template in `templates/sessionview.pt`. + +:::{note} +*Display view* 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 `templates/sessionview.pt` template contains the following: + +```html + + + + + +
+

+
+

+

+
+
+ + + + + +``` + +Notice how we use expressions like `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. + +[z3c.form]: http://pypi.python.org/pypi/z3c.form diff --git a/plone.app.dexterity/designing.md b/plone.app.dexterity/designing.md new file mode 100644 index 000000000..e45519f3e --- /dev/null +++ b/plone.app.dexterity/designing.md @@ -0,0 +1,42 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# 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 modelled 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 bios for presenters. + +There are many ways to approach this, but here is one possible design: + +- A content type Presenter is used to represent presenter bios. + 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..fd265f55f --- /dev/null +++ b/plone.app.dexterity/index.md @@ -0,0 +1,34 @@ +--- +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 + +```{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..4e63bda3e --- /dev/null +++ b/plone.app.dexterity/install.md @@ -0,0 +1,29 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Installing Dexterity + +How to install Dexterity and use it in your project. + +```{note} +Dexterity is an **already installed part of Plone 5.x**, no action is needed here. +``` + +## Installing Dexterity on Plone 4.3 + +Dexterity is included with Plone 4.3, but must be activated via the "Add-ons" configlet in site setup. + +```{important} +If you installed Dexterity on a Plone site that you upgraded to Plone 4.3, you must include the relations extra `plone.app.dexterity [relations]`. +Otherwise your site will have a broken intid utility. +``` + +Dexterity is distributed as a number of eggs, published on [PyPI](https://pypi.org). +The [plone.app.dexterity](https://pypi.org/project/plone.app.dexterity/) egg pulls in all the required dependencies and should get you up and running. +This how-to explains what you need to do to use Dexterity in a standard Plone buildout. 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..b088162b5 --- /dev/null +++ b/plone.app.dexterity/model-driven-types.md @@ -0,0 +1,115 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# 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 great 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 contents: + +```xml + + + + + Name + + + + A short summary + + + + False + Bio + + + Please upload an image. + False + Photo + + + +``` + +The XML name spaces we use are described in the `Dexterity XML` reference section. + +Now, open the `presenter.py` that we created in the last section (a copy of our original program.py). +Delete the field declarations from the IPresenter class and edit to read: + +```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 for schema-driven type. + +Look in the `types.xml` file in your packages +`example/conference/profiles/default` directory. It should now contain: + +```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 addable anywhere. diff --git a/plone.app.dexterity/prerequisite.md b/plone.app.dexterity/prerequisite.md new file mode 100644 index 000000000..699b59182 --- /dev/null +++ b/plone.app.dexterity/prerequisite.md @@ -0,0 +1,169 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Prerequisites + +This portion of the Dexterity documentation is mainly intended 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 to 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 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..6166753a4 --- /dev/null +++ b/plone.app.dexterity/reference/dexterity-xml.md @@ -0,0 +1,344 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Dexterity XML + +**A reference for Dexterity's XML name spaces** + +## Introduction + +The schema (structure) of a Dexterity content type may be detailed in two very different ways: + +> - In Python as a Zope schema; or, +> - In XML + +When you are using Dexterity's through-the-web 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 like: + +```python +class IExampleType(form.Schema): + + form.model("models/example_type.xml") +``` + +or: + +``` +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: + +```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 (as if it was being imported in Python) of the field type. + +### Fieldsets + +It's easy to add fieldsets by surrounding embedding fields 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: + +``` +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 + +``` + +With Python like: + +```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 re-order 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 field in the +current schema (or a base schema). Use a fully prefixed name (e.g. +`'my.package.ISomeSchema'`) to refer to a field in another schema. Use an +unprefixed name to refer to a field in the default schema for the form. + +Example: + +```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`. + +Example: + +```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 like +`form:omitted="z3c.form.interfaces.IForm:true"`. Multiple interface:value +settings may be specified, separated by spaces. + +Examples: + +```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. + +Example: + +```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. + +Example: + +```xml + + Three + plone.supermodel.tests.dummy_defaultFactory + +``` + +Sample Python for the validator factory: + +```python +@provider(IDefaultFactory) +def dummy_defaultFactory(): + return u'something' +``` + +For a callable using context: + +```python +@provider(IContextAwareDefaultFactory) +def dummy_defaultCAFactory(context): + return context.something +``` + +:::{note} +The `defaultFactory` tag was added in plone.supermodel 1.2.3, +shipping with Plone 4.3.2+. +::: + +### 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. + +Example: + +```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/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. + +Example: + +```xml + + One + +``` diff --git a/plone.app.dexterity/reference/fields.md b/plone.app.dexterity/reference/fields.md new file mode 100644 index 000000000..8989f6639 --- /dev/null +++ b/plone.app.dexterity/reference/fields.md @@ -0,0 +1,131 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Fields + +**The standard schema fields** + +The following tables shows the most common field types for use in +Dexterity schemata. +See the documentation on [creating schemata] for information about how to +use these. + +## Field properties + +Fields are initialised 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 | unicode | 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/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, e.g. for an `Int` field this should be an integer. Default is `None` (no check). | +| | max | | The maximum allowed value. Must be a valid value for the field, e.g. for an Int field this 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 `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 [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, i.e. a `Bytes` with newlines 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 (e.g. 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]. Not available by default in Zope 2.10. | IField, IMinMax | + +### Fields in `plone.namedfile.field` + +See [plone.namedfile] and [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] 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] 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] for more details. + +| Name | Type | Description | Properties | +| ----- | ---- | ----------------------------------- | ------------------ | +| Email | str | A field containing an email address | IField, IMinMaxLen | + +[creating schemata]: ../schema-driven-types.html#the-schema +[plone.app.textfield]: http://pypi.python.org/pypi/plone.app.textfield +[plone.formwidget.namedfile]: http://pypi.python.org/pypi/plone.formwidget.namedfile +[plone.namedfile]: http://pypi.python.org/pypi/plone.namedfile +[plone.schema]: http://pypi.python.org/pypi/plone.schema +[vocabularies]: ../advanced/vocabularies.html +[z3c.relationfield]: http://pypi.python.org/pypi/z3c.relationfield +[zope.schema]: http://pypi.python.org/pypi/zope.schema 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..496457f89 --- /dev/null +++ b/plone.app.dexterity/reference/form-schema-hints.md @@ -0,0 +1,159 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Form Configuration with Schema Hints using Directives + +Dexterity uses the directives in [plone.autoform](http://pypi.python.org/pypi/plone.autoform) and [plone.supermodel](http://pypi.python.org/pypi/plone.supermodel) package to configure its [z3c.form](http://docs.zope.org/z3c.form)-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](http://pypi.python.org/pypi/plone.autoform) and [plone.supermodel](http://pypi.python.org/pypi/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 they apply to. + +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=u'Title') + + directives.omitted('additionalInfo') + additionalInfo = schema.Bytes() +``` + +The form directives are taking 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 these: + +| 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 (i.e. one from a behavior), its name will be e.g. `IOtherSchema.other_field_name`. If the other field is from the same schema, its name can be abbreviated by a leading dot e.g. `.other_field_name`. If the other field is is used without a prefix, its is looked up from the main schema e.g. `other_field_name`. Alternatively, pass the string “\*” to put a field first in the fieldsets form. | +| 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 these: + +| 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=u'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=u'Body text', + required=False, + default=u'Body text goes here' + ) + + # The widget can also be specified as an object + + directives.widget(footer=WysiwygFieldWidget) + footer = schema.Text( + title=u'Footer text', + required=False + ) + + # An omitted field. + # Use directives.omitted('a', 'b', 'c') to omit several fields + + directives.omitted('dummy') + dummy = schema.Text( + title=u'Dummy' + ) + + # A field in 'hidden' mode + + directives.mode(secret='hidden') + secret = schema.TextLine( + title=u'Secret', + default=u'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=u'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=u'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..8181b7f0b --- /dev/null +++ b/plone.app.dexterity/reference/index.md @@ -0,0 +1,24 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Reference + +**Useful references for things like field types, wigets and APIs** + +```{toctree} +:maxdepth: 2 + +fields.rst +widgets.rst +standard-behaviours.rst +form-schema-hints.rst +manipulating-content-objects.rst +dexterity-xml.rst +misc.rst +``` 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..3548d1d30 --- /dev/null +++ b/plone.app.dexterity/reference/manipulating-content-objects.md @@ -0,0 +1,382 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Manipulating 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. +::: + +**Common APIs used to manipulate Dexterity content objects** + +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 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 acquisition wrapped. You can wrap it +explicitly by calling: + +```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=u"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 (e.g. a *Large Plone Folder* in Plone 3 or a container based on +*plone.folder*), you can do: + +```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, i.e. 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, so things like 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=u"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, and you can all *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 so 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 means of getting information about an object. + +### Obtaining an object’s schema interface + +A content object’s schema is an interface, i.e. an object of type +*zope.interface.interface.InterfaceClass*. + +```python +from zope.app.content import queryContentType +schema = queryContentType(context) +``` + +The schema can now be inspected. For example: + +```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: + +```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 containment 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: + +```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 actually 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: + +```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 [permissions]. + +### Checking a permission + +To check a permission by its Zope 3 name: + +```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 (e.g. the *afterSetUp()* method). + +To 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: + +```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 1st, 1970. | +| modified() | DateTime | Returns the Zope 2 DateTime for the object’s modification date. If not set, returns a “floor” date of January 1st, 1970. | +| effective() | DateTime | Returns the Zope 2 DateTime for the object’s effective date. If not set, returns a “floor” date of January 1st, 1970. | +| expires() | DateTime | Returns the Zope 2 DateTime for the object’s expiration date. If not set, returns a “floor” date of January 1st, 1970. | + +[permissions]: ../advanced/permissions.html diff --git a/plone.app.dexterity/reference/misc.md b/plone.app.dexterity/reference/misc.md new file mode 100644 index 000000000..39482a445 --- /dev/null +++ b/plone.app.dexterity/reference/misc.md @@ -0,0 +1,30 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Miscellaneous + +## User contributed recipes + +**How to hide a field on a schema if we do not want to or cannot modify the original schema** + +To do this one can use tagged values on the schema. In this case want to hide '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 sit in another.package.\_\_init\_\_.py for example. + +See also: [Original thread on coredev mailinglist] + +[original thread on coredev mailinglist]: http://plone.293351.n2.nabble.com/plone-autoform-why-use-tagged-values-td7560956.html diff --git a/plone.app.dexterity/reference/standard-behaviours.md b/plone.app.dexterity/reference/standard-behaviours.md new file mode 100644 index 000000000..e81228e30 --- /dev/null +++ b/plone.app.dexterity/reference/standard-behaviours.md @@ -0,0 +1,38 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Standard behaviors + +**A list of 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) and/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..ad0e3f803 --- /dev/null +++ b/plone.app.dexterity/reference/widgets.md @@ -0,0 +1,37 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Widgets + +**Standard and common third party widgets** + +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][z3c.form documentation]. To learn about setting custom widgets for Dexterity +content types, see the [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 [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/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. | + +[schema introduction]: ../schema-driven-types.html#the-schema +[vocabularies]: ../advanced/vocabularies.html +[z3c.form documentation]: https://z3cform.readthedocs.io/en/latest/widgets/index.html diff --git a/plone.app.dexterity/schema-driven-types.md b/plone.app.dexterity/schema-driven-types.md new file mode 100644 index 000000000..371dba0d2 --- /dev/null +++ b/plone.app.dexterity/schema-driven-types.md @@ -0,0 +1,393 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Schema-driven types + +How to create a minimal type based on a schema. + +## The schema + +A simple Dexterity type consists of a schema and an FTI (Factory Type Information, 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 up a bit and then add another content type. + +:::{note} +The template's original setup assumed we were adding 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** + +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 "Adding Sessions" 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: + +```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=_(u'Program name'), + ) + + description = schema.Text( + title=_(u'Program summary'), + ) + + start = schema.Datetime( + title=_(u'Start date'), + required=False, + ) + + end = schema.Datetime( + title=_(u'End date'), + required=False, + ) + + details = RichText( + title=_(u'Details'), + description=_(u'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 internationalisation 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 this: + +```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=_(u'Title'), + description=_(u'Session title'), + ) + + description = schema.Text( + title=_(u'Session summary'), + ) + + details = RichText( + title=_(u'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 vs. 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] package. +You should look at its interfaces (`parts/omelette/zope/schema/interfaces.py`) to learn about the various schema fields available, and review the [online documentation] for the package. +You may also want to look up [plone.namedfile], which you can use if you require a file field, [plone.app.relationfield], which can be used for references, and [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] package) to construct forms. +Take a look at the [plone.autoform] documentation 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 + +**Adding a Factory Type Information object for the type** + +When we created `types/session.xml` and `types/presenter.xml` files 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 packages `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 matche the type name. + +Let's take a look at a `types/` XML file. +The `Session` type, in `session.xml`, should look like this: + +```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 re-usable aspects providing semantics and/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 this: + +```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 + +**How to start up Plone and test the type, and 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 a buildout, and then start `./bin/instance fg` as normal. +Add a Plone site, and go to the 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 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 develop egg. + `develop = src/*` should suffice, but you can also add the package explicitly, e.g. 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 Add-ons control panel: + +- Look for errors in the {guilabel}`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 (e.g. you are missing custom widgets): + +- 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. + +[online documentation]: http://pypi.python.org/pypi/zope.schema +[plone.app.relationfield]: http://pypi.python.org/pypi/plone.app.relationfield +[plone.app.textfield]: http://pypi.python.org/pypi/plone.app.textfield +[plone.autoform]: http://pypi.python.org/pypi/plone.autoform +[plone.namedfile]: http://pypi.python.org/pypi/plone.namedfile +[zope.schema]: http://pypi.python.org/pypi/zope.schema diff --git a/plone.app.dexterity/testing/index.md b/plone.app.dexterity/testing/index.md new file mode 100644 index 000000000..8dc556303 --- /dev/null +++ b/plone.app.dexterity/testing/index.md @@ -0,0 +1,20 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Testing Dexterity types + +**Writing unit and integration tests** + +```{toctree} +:maxdepth: 2 + +unit-tests.rst +integration-tests.rst +mock-testing.rst +``` diff --git a/plone.app.dexterity/testing/integration-tests.md b/plone.app.dexterity/testing/integration-tests.md new file mode 100644 index 000000000..a8d11fa1b --- /dev/null +++ b/plone.app.dexterity/testing/integration-tests.md @@ -0,0 +1,385 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Integration tests + +**Writing integration tests with 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 re-used 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, erm, 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 *setup.py*, we will add the extras_require option, like so: + +``` +extras_require = { + 'test': ['plone.app.testing'] +}, +``` + +:::{note} +Don’t forget to re-run buildout after making changes to `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/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 +`testing.py` file: + +``` +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* 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.): + +``` +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] 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 (here, 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), + 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 (e.g. 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. + +```console +$ ./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 `tests/test_session.py` to test +the `Session` type: + +``` +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: + +``` +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__) +``` + +[collective.testcaselayer]: http://pypi.python.org/pypi/collective.testcaselayer +[plone.app.testing]: http://pypi.python.org/pypi/plone.app.testing +[testing tutorial]: /external/plone.app.testing/docs/source/index diff --git a/plone.app.dexterity/testing/mock-testing.md b/plone.app.dexterity/testing/mock-testing.md new file mode 100644 index 000000000..85ca0532c --- /dev/null +++ b/plone.app.dexterity/testing/mock-testing.md @@ -0,0 +1,263 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Mock testing + +**Using 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 like this: + +``` +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, e.g. 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, or the number of times a method is + called or an attribute is retrieved or set. +- You can also give your mock objects behaviour, e.g. by specifying + return values or exceptions to be raised in certain cases. +- Initialise the code under test and/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 (e.g. with the wrong arguments, or too many times) + or insufficiently (e.g. an expected method was not called). + +There are several Python mock object frameworks. Dexterity itself users +a powerful one called [mocker], via the [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 *setup.py* and re-run +buildout. + +``` +install_requires=[ + ... + 'plone.mocktestcase', +], +``` + +As an example test case, consider the following class in +*test_presenter.py*: + +``` +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, + e.g. 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, e.g.: + +``` +$ ./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, for example by [Martin Fowler]. + +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. + +[martin fowler]: http://www.martinfowler.com/articles/mocksArentStubs.html +[mocker]: http://labix.org/mocker +[plone.mocktestcase]: http://pypi.python.org/pypi/plone.mocktestcase diff --git a/plone.app.dexterity/testing/unit-tests.md b/plone.app.dexterity/testing/unit-tests.md new file mode 100644 index 000000000..05c9c4353 --- /dev/null +++ b/plone.app.dexterity/testing/unit-tests.md @@ -0,0 +1,171 @@ +--- +myst: + html_meta: + "description": "" + "property=og:description": "" + "property=og:title": "" + "keywords": "" +--- + +# Unit tests + +**Writing simple unit tests** + +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]. +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, because: + +- 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’re writing content types, we’re +often more interested in integration test, 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 `program.py`. +We’ll add the directory `tests`, with an `__init__.py` and a file +`test_program.py` that looks like this: + +``` +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 simple 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/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, we can do: + +```console +$ ./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 +(like output colouring). +We could also use the built-in test runner in the `instance` script, +e.g. with `./bin/instance test -s example.conference`. +::: + +To run just this test suite, we can do: + +```console +$ ./bin/test example.conference -t TestProgramUnit +``` + +This is useful when we have other test suites that we don’t want to run, +e.g. because they are integration tests and require lengthy setup. + +To get a report about test coverage, we can run: + +```console +$ ./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%. + +[plone testing tutorial]: /external/plone.app.testing/docs/source/index From ca037d973fa61bf2e148b89f90023d9b0e8351a0 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 5 Mar 2023 23:17:29 -0800 Subject: [PATCH 06/53] Add introductory sentence --- plone.app.dexterity/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plone.app.dexterity/index.md b/plone.app.dexterity/index.md index fd265f55f..bd7682b80 100644 --- a/plone.app.dexterity/index.md +++ b/plone.app.dexterity/index.md @@ -9,6 +9,8 @@ myst: # Content types developer manual +This part of the documentation describes how to develop content types in Plone. + ```{toctree} :maxdepth: 2 From 3ec8e9d981d091aaea6ecb463fc4d6fcefed86ca Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 5 Mar 2023 23:20:45 -0800 Subject: [PATCH 07/53] Tidy designing.md --- plone.app.dexterity/designing.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plone.app.dexterity/designing.md b/plone.app.dexterity/designing.md index e45519f3e..1cb08802c 100644 --- a/plone.app.dexterity/designing.md +++ b/plone.app.dexterity/designing.md @@ -1,10 +1,10 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 @@ -16,7 +16,7 @@ Plone uses the ZODB, an object database, instead of a relational database as its 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 modelled as a container (the "one") containing many items (the "many"), although it is also possible to use references across the content hierarchy. +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. @@ -26,11 +26,11 @@ When we attempt to solve a particular content management problem with Plone, we 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 bios for presenters. +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 bios. +- 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. From 665d100f35aa380768e0da228ef35e4e3b8d408a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 5 Mar 2023 23:27:08 -0800 Subject: [PATCH 08/53] Tidy prerequisite.md --- plone.app.dexterity/prerequisite.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/plone.app.dexterity/prerequisite.md b/plone.app.dexterity/prerequisite.md index 699b59182..61fa21306 100644 --- a/plone.app.dexterity/prerequisite.md +++ b/plone.app.dexterity/prerequisite.md @@ -1,15 +1,15 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 is mainly intended to illuminate Dexterity features. +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. @@ -58,7 +58,7 @@ 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 to our theme and other customizations. +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. @@ -75,13 +75,13 @@ We run the following from the `src/` directory 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 +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 Dexterity base class (remember that Programs will contain Sessions). +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. @@ -109,7 +109,7 @@ Now let's take a look at `configure.zcml` in the `examples/conference` directory Again, we want to note a few parts: ```xml - + @@ -135,8 +135,6 @@ Again, we want to note a few parts: post_handler=".setuphandlers.uninstall" /> - ... - ``` @@ -146,7 +144,7 @@ The alternative would be to manually add a line, such as ` Date: Sun, 5 Mar 2023 23:50:08 -0800 Subject: [PATCH 09/53] Tidy schema-driven-types.md --- plone.app.dexterity/schema-driven-types.md | 230 +++++++++++---------- 1 file changed, 120 insertions(+), 110 deletions(-) diff --git a/plone.app.dexterity/schema-driven-types.md b/plone.app.dexterity/schema-driven-types.md index 371dba0d2..0b72b90a9 100644 --- a/plone.app.dexterity/schema-driven-types.md +++ b/plone.app.dexterity/schema-driven-types.md @@ -1,40 +1,44 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -How to create a minimal type based on a schema. +This chapter describes how to create a minimal type based on a schema. + ## The schema -A simple Dexterity type consists of a schema and an FTI (Factory Type Information, the object configured in {guilabel}`portal_types` in the ZMI). -We’ll create the schemata here, and the FTI on the next page. +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 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 up a bit and then add another content type. +## 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 were adding a single content type. In fact, we're going to add three. +```{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`. +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`. +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 @@ -49,18 +53,22 @@ and change it to read: That makes our setup profile point to our renamed schema file. -**Adding Sessions** -Now, let's add another content type for our conference sessions. +(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`. +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". +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: @@ -71,14 +79,15 @@ Find the `src/example/conference/profiles/types.xml` file, add a new object decl ``` -Repeat the "Adding Sessions" steps for presenter.py, IPresenter, and presenter.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. +Start with `program.py`. +Add schema declarations for our `start`, `end`, and `details` fields. -The top part of the file should look like: +The top part of the file should look like the following. ```python from example.conference import _ @@ -92,35 +101,35 @@ class IProgram(model.Schema): """A conference program. Programs can contain Sessions.""" title = schema.TextLine( - title=_(u'Program name'), + title=_('Program name'), ) description = schema.Text( - title=_(u'Program summary'), + title=_('Program summary'), ) start = schema.Datetime( - title=_(u'Start date'), + title=_('Start date'), required=False, ) end = schema.Datetime( - title=_(u'End date'), + title=_('End date'), required=False, ) details = RichText( - title=_(u'Details'), - description=_(u'Details about the program.'), + 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 internationalisation of the package. -Every string that is presented to the user should be wrapped in `_()` as shown with the titles and descriptions below. +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: +The `_` lives in the package root `__init__.py` file. ```python from zope.i18nmessageid import MessageFactory @@ -130,13 +139,13 @@ _ = 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). +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. +Save `program.py`. -`session.py` for the Session type should look like this: +`session.py` for the Session type should look like the following. ```python from example.conference import _ @@ -150,49 +159,51 @@ class ISession(model.Schema): """A conference session. Sessions are managed inside Programs.""" title = schema.TextLine( - title=_(u'Title'), - description=_(u'Session title'), + title=_('Title'), + description=_('Session title'), ) description = schema.Text( - title=_(u'Session summary'), + title=_('Session summary'), ) details = RichText( - title=_(u'Session details'), + 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. +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 vs. other interfaces +## 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] package. -You should look at its interfaces (`parts/omelette/zope/schema/interfaces.py`) to learn about the various schema fields available, and review the [online documentation] for the package. -You may also want to look up [plone.namedfile], which you can use if you require a file field, [plone.app.relationfield], which can be used for references, and [Plone.app.textfield], which supports rich text with a WYSIWYG editor. +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] package) to construct forms. -Take a look at the [plone.autoform] documentation to learn more about the various hints that are possible. +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 +## Factory Type Information (FTI) -**Adding a Factory Type Information object for the type** +This section describes how to add a Factory Type Information (FTI) object for the type. -When we created `types/session.xml` and `types/presenter.xml` files and added object declarations to `types.xml`, we made our new content types installable. +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 packages `example/conference/profiles/default` directory: +Look in the `types.xml` file in your package's `example/conference/profiles/default` directory. ```xml @@ -204,12 +215,12 @@ Look in the `types.xml` file in your packages `example/conference/profiles/defau 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*. +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 matche the type name. +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 this: +The `Session` type, in `session.xml`, should look like the following. ```xml @@ -295,26 +306,27 @@ However, it is useful to see the options available so that you know what you can 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 re-usable aspects providing semantics and/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 this: +- 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 @@ -345,49 +357,47 @@ The `Program`, in `program.xml`, looks like this: ``` -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. +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 -**How to start up Plone and test the type, and some trouble-shooting tips.** +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 a buildout, and then start `./bin/instance fg` as normal. -Add a Plone site, and go to the Add-ons control panel. -You should see your package there and be able to install it. +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 Zope doesn't start up: -If you don’t see your package in the Add-ons control panel: +- 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. -- 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 develop egg. - `develop = src/*` should suffice, but you can also add the package explicitly, e.g. 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 you don't see your package in the {guilabel}`Add-ons` control panel: -If the package fails to install in the 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`. -- Look for errors in the {guilabel}`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 the package fails to install in the {guilabel}`Add-ons` control panel: -If your forms do not look right (e.g. you are missing custom widgets): +- 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`. -- 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. +If your forms do not look right, for example custom widgets are missing, then: -[online documentation]: http://pypi.python.org/pypi/zope.schema -[plone.app.relationfield]: http://pypi.python.org/pypi/plone.app.relationfield -[plone.app.textfield]: http://pypi.python.org/pypi/plone.app.textfield -[plone.autoform]: http://pypi.python.org/pypi/plone.autoform -[plone.namedfile]: http://pypi.python.org/pypi/plone.namedfile -[zope.schema]: http://pypi.python.org/pypi/zope.schema +- 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. From 147f9c6b213289df7b16522739f139b56b937259 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 5 Apr 2023 05:52:41 -0700 Subject: [PATCH 10/53] Tidy model-driven-types.md --- plone.app.dexterity/model-driven-types.md | 35 ++++++++++++----------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/plone.app.dexterity/model-driven-types.md b/plone.app.dexterity/model-driven-types.md index b088162b5..6f5180e70 100644 --- a/plone.app.dexterity/model-driven-types.md +++ b/plone.app.dexterity/model-driven-types.md @@ -1,10 +1,10 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "description": "Model-driven types" + "property=og:description": "Model-driven types" + "property=og:title": "Model-driven types" + "keywords": "Plone, model, content types" --- # Model-driven types @@ -12,15 +12,16 @@ myst: 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 great 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. +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 contents: +In it, add a `presenter.xml` file with the following content. ```xml ``` -The XML name spaces we use are described in the `Dexterity XML` reference section. +The XML name spaces we use are described in the {doc}`reference/dexterity-xml` reference chapter. -Now, open the `presenter.py` that we created in the last section (a copy of our original program.py). -Delete the field declarations from the IPresenter class and edit to read: +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 _ @@ -61,21 +62,21 @@ from zope import schema class IPresenter(model.Schema): - """Schema for Conference Presenter content type.""" - model.load('models/presenter.xml') + 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 for schema-driven type. +## Setting factory type information + +This part of the process is identical to what we explained in {doc}`schema-driven-types`. -Look in the `types.xml` file in your packages -`example/conference/profiles/default` directory. It should now contain: +Look in the {file}`types.xml` file in your package's `example/conference/profiles/default` directory. +It should now contain the following code. ```xml @@ -85,7 +86,7 @@ Look in the `types.xml` file in your packages ``` -For the *Presenter* type, we have `example.conference.presenter.xml`: +For the `Presenter` type, we have `example.conference.presenter.xml`. ```xml @@ -112,4 +113,4 @@ For the *Presenter* type, we have `example.conference.presenter.xml`: ``` -Note that this is addable anywhere. +Note that this is may be added anywhere. From b6d57e4720e661c1418b6bf4bcff08d5f68feebc Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 5 Apr 2023 06:08:25 -0700 Subject: [PATCH 11/53] Tidy custom-views.md --- plone.app.dexterity/custom-views.md | 106 ++++++++++++---------------- 1 file changed, 46 insertions(+), 60 deletions(-) diff --git a/plone.app.dexterity/custom-views.md b/plone.app.dexterity/custom-views.md index 1ce6cd1b4..3252c7f05 100644 --- a/plone.app.dexterity/custom-views.md +++ b/plone.app.dexterity/custom-views.md @@ -1,39 +1,36 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "description": "Configure custom views" + "property=og:description": "Configure custom views" + "property=og:title": "Configure custom views" + "keywords": "Plone, configure, custom views" --- # Custom views -**Configuring custom views and using display forms** +This chapter describes how to configure custom views and use display forms. -## Simple views -**Creating basic views** +## Simple views -So far, our types have used the default views. -They use the *display* widgets from [z3c.form], much like the add and edit forms use the *edit* widgets. +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 FTI configuration. -This is because the FTI’s `default_view` property is set to `view`, and `view` is in the list of `view_methods.` +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 `` `configure.zcml `` file: +First create a view registration with a `` ZCML directive in your {file}`configure.zcml` file. ```xml - ... - ` ZCML directive in you ``` -Secondly add a browser view in `program.py` as follows: +Next add a browser view in `program.py` as follows. ```python from Acquisition import aq_inner @@ -74,7 +71,7 @@ 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 `templates/programview.pt`: +Finaly add a template in {file}`templates/programview.pt`. ```html `. -::: +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 even simpler: +The view for `Presenter` is the following. ```xml - ... - ``` -The template, in `templates/presenterview.pt`, is similar to the previous template: +The template in {file}`templates/presenterview.pt` is similar to the previous template. ```html - ... - ``` -Notice how we use expressions like `view/w/details/render` (where `details` is the field name) to get the rendering of a widget. +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. - -[z3c.form]: http://pypi.python.org/pypi/z3c.form From 1275e3c114cb3c40328e740af9c0ce779186ecfa Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 5 Apr 2023 06:13:45 -0700 Subject: [PATCH 12/53] Tidy install.md --- plone.app.dexterity/install.md | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/plone.app.dexterity/install.md b/plone.app.dexterity/install.md index 4e63bda3e..ae024b086 100644 --- a/plone.app.dexterity/install.md +++ b/plone.app.dexterity/install.md @@ -1,29 +1,21 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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" --- -# Installing Dexterity +# Install Dexterity -How to install Dexterity and use it in your project. +This chapter describes how to install Dexterity and use it in your project. ```{note} -Dexterity is an **already installed part of Plone 5.x**, no action is needed here. +Dexterity is already installed as part of Plone 5.x and later. +No further action is needed for these versions. ``` -## Installing Dexterity on Plone 4.3 +## Install Dexterity on Plone 4.3 -Dexterity is included with Plone 4.3, but must be activated via the "Add-ons" configlet in site setup. - -```{important} -If you installed Dexterity on a Plone site that you upgraded to Plone 4.3, you must include the relations extra `plone.app.dexterity [relations]`. -Otherwise your site will have a broken intid utility. -``` - -Dexterity is distributed as a number of eggs, published on [PyPI](https://pypi.org). -The [plone.app.dexterity](https://pypi.org/project/plone.app.dexterity/) egg pulls in all the required dependencies and should get you up and running. -This how-to explains what you need to do to use Dexterity in a standard Plone buildout. +See [Installing Dexterity](https://4.docs.plone.org/external/plone.app.dexterity/docs/install.html). From 8a89be56cd382e6d1239ffd670953837d9ee2c8e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 5 Apr 2023 06:15:38 -0700 Subject: [PATCH 13/53] Tidy advanced/index.md --- plone.app.dexterity/advanced/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plone.app.dexterity/advanced/index.md b/plone.app.dexterity/advanced/index.md index 885fda59e..993ebe55c 100644 --- a/plone.app.dexterity/advanced/index.md +++ b/plone.app.dexterity/advanced/index.md @@ -1,15 +1,15 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Further configuration and tips** +This part describes further configuration and tips for content types in Plone. ```{toctree} :maxdepth: 2 From cbacb7200dd36bfb16fc3eb0cc0274e60834e341 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 5 Apr 2023 06:16:09 -0700 Subject: [PATCH 14/53] Strip file extensions --- plone.app.dexterity/advanced/index.md | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/plone.app.dexterity/advanced/index.md b/plone.app.dexterity/advanced/index.md index 993ebe55c..fc8a21291 100644 --- a/plone.app.dexterity/advanced/index.md +++ b/plone.app.dexterity/advanced/index.md @@ -14,19 +14,19 @@ This part describes further configuration and tips for content types in Plone. ```{toctree} :maxdepth: 2 -defaults.rst -validators.rst -vocabularies.rst -references.rst -rich-text-markup-transformations.rst -files-and-images.rst -static-resources.rst -behaviours.rst -event-handlers.rst -permissions.rst -workflow.rst -catalog-indexing-strategies.rst -custom-add-and-edit-forms.rst -custom-content-classes.rst -webdav-and-other-file-representations.rst +defaults +validators +vocabularies +references +rich-text-markup-transformations +files-and-images +static-resources +behaviours +event-handlers +permissions +workflow +catalog-indexing-strategies +custom-add-and-edit-forms +custom-content-classes +webdav-and-other-file-representations ``` From 781f956c7a812ddd181b0dcfc62f8f24ea5b1da8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 7 Apr 2023 03:00:05 -0700 Subject: [PATCH 15/53] Tidy defaults.md --- plone.app.dexterity/advanced/defaults.md | 43 ++++++++++-------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/plone.app.dexterity/advanced/defaults.md b/plone.app.dexterity/advanced/defaults.md index 7d8d8f556..314c69a74 100644 --- a/plone.app.dexterity/advanced/defaults.md +++ b/plone.app.dexterity/advanced/defaults.md @@ -1,23 +1,21 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "description": "Content type defaults" + "property=og:description": "Content type defaults" + "property=og:title": "Content type defaults" + "keywords": "Plone, Content type, defaults" --- # Defaults -**Default values for fields on add forms** +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. +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 -`program.py`: +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 @@ -29,8 +27,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`: +We also need to modify `IProgram` so the `start` and `end` fields use these functions as their `defaultFactory`. ```python class IProgram(model.Schema): @@ -48,15 +45,12 @@ class IProgram(model.Schema): ) ``` -The `defaultFactory` is a function that will be called when the add form -is loaded to determine the default value. +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. +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: +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 @@ -67,10 +61,7 @@ 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] documentation for more details. +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. - -[z3c.form]: https://pypi.python.org/pypi/z3c.form#look-up-value-from-default-adapter +We'll cover creating custom forms later in this manual. From e366c16105b688846bb942d1c54a197493d4511c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 06:07:47 -0700 Subject: [PATCH 16/53] Tidy validators.md --- plone.app.dexterity/advanced/validators.md | 114 ++++++++------------- 1 file changed, 45 insertions(+), 69 deletions(-) diff --git a/plone.app.dexterity/advanced/validators.md b/plone.app.dexterity/advanced/validators.md index 8ecf195ed..9a6166e6c 100644 --- a/plone.app.dexterity/advanced/validators.md +++ b/plone.app.dexterity/advanced/validators.md @@ -1,122 +1,98 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "description": "Validators" + "property=og:description": "Validators" + "property=og:title": "Validators" + "keywords": "Plone, Validators, constraints, content types" --- # Validators -**Creating custom validators for your type** +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] library -ensures that all data entered on Dexterity add and edit forms is valid -for the field 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] package for details, but -the most common constraints are: +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. -`required=True/False` +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; +: 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. -: 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 of the field’s type. The function -should return a boolean `True` or `False`. +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} -Hint: 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. -::: +```{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, e.g.: +To use the constraint, pass the function as the `constraint` argument to the field constructor, for example: -``` +```python my_field = schema.TextLine(title=_(u"My field"), constraint=checkForMagic) ``` -Constraints are easy to write, but do not necessarily produce very -friendly error messages. It is however possible to customise these error -messages using [z3c.form] error view snippets. See the [z3c.form -documentation](z3c.form) for more details. +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. +## Invariants -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. -Code not relevant to this example is snipped with an ellipsis (…): +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__ = _(u"The start or end date is invalid") + __doc__ = _("The start or end date is invalid") class IProgram(model.Schema): - ... - start = schema.Datetime( - title=_(u"Start date"), - required=False, - ) + title=_("Start date"), + required=False, + ) end = schema.Datetime( - title=_(u"End date"), - required=False, - ) - - ... + 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(_(u"The start date must be before the end date.")) - -... + 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](http://pypi.python.org/pypi/z3c.form) -widget validators. For details see the [z3c.form validators section](http://docs.plone.org/develop/plone/forms/z3c.form.html#validators). +## Form validators -[zope.schema]: http://pypi.python.org/pypi/zope.schema +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). From 84677d820a43b4cf9443df7533e8868bc57d5f1a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 06:29:36 -0700 Subject: [PATCH 17/53] Tidy vocabularies.md --- plone.app.dexterity/advanced/vocabularies.md | 220 ++++++++----------- 1 file changed, 90 insertions(+), 130 deletions(-) diff --git a/plone.app.dexterity/advanced/vocabularies.md b/plone.app.dexterity/advanced/vocabularies.md index d29b8f714..0fa729730 100644 --- a/plone.app.dexterity/advanced/vocabularies.md +++ b/plone.app.dexterity/advanced/vocabularies.md @@ -1,55 +1,50 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" + "description": "Vocabularies" + "property=og:description": "Vocabularies" + "property=og:title": "Vocabularies" "keywords": "" --- # Vocabularies -**Creating your own static and dynamic 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] package, -with widgets provided by [z3c.form]. +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: +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`: +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: +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; +- `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. -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 organiser responsible for the program. ## Static vocabularies -Our first attempt uses a static list of organisers. -We use the message factory to allow the labels (term titles) 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: +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 @@ -57,14 +52,14 @@ from zope.schema.vocabulary import SimpleVocabulary organizers = SimpleVocabulary( [ - SimpleTerm(value=u'Bill', title=_(u'Bill')), - SimpleTerm(value=u'Bob', title=_(u'Bob')), - SimpleTerm(value=u'Jim', title=_(u'Jim')) + SimpleTerm(value='Bill', title=_('Bill')), + SimpleTerm(value='Bob', title=_('Bob')), + SimpleTerm(value='Jim', title=_('Jim')) ] ) organizer = schema.Choice( - title=_(u"Organiser"), + title=_('organizer"), vocabulary=organizers, required=False, ) @@ -72,18 +67,16 @@ organizer = schema.Choice( 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. +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 simply a callable (usually a function or an object with a `__call__` method). +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 -(i.e. 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]. +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: @@ -115,67 +108,56 @@ def possibleOrganizers(context): return SimpleVocabulary(terms) ``` -We use the PluggableAuthService API to get the group and its members. +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 *tokenised* 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. +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 tokenised vocabulary where - the values are the items in the list, and the tokens are created by - calling `str()` on the values. +: 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. -: takes a list of `(token, value)` tuples and creates a tokenised - vocabulary with the token and value specified. - -You can also instantiate a `SimpleVocabulary` yourself and pass a list -of terms in the initialiser. -The `createTerm()` class method can be used to create a term from a -`value`, `token` and `title`. Only the value is required. +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. +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. +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=_(u"Organiser"), + title=_('organizer"), source=possibleOrganizers, required=False, ) ``` -## Parameterised 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 initialised with the group name: +## 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 @@ -210,42 +192,35 @@ class GroupMembers(object): return SimpleVocabulary(terms) ``` -Again, the source is set using the `source` argument to the `Choice` -constructor: +Again, the source is set using the `source` argument to the `Choice` constructor: ```python organizer = schema.Choice( - title=_(u"Organiser"), + title=_('organizer"), source=GroupMembers('organizers'), required=False, ) ``` -When the schema is initialised on startup, a `GroupMembers` object -is instantiated, storing the desired group name. Each time the -vocabulary is needed, this object will be called (i.e. the -`__call__()` method is invoked) with the context as an argument, -expected to return an appropriate vocabulary. +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 re-usable, since you can import the source from a single location and use it in multiple instances. +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. +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 parameterised in the way as we did with the `GroupMembers` context source binder, -since they are looked up by name only. -::: +```{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 `vocabularies.py`: +Create a vocabulary factory in {file}`vocabularies.py`: ```python from zope.schema.interfaces import IVocabularyFactory @@ -272,73 +247,67 @@ def organizers_vocabulary_factory(context): return SimpleVocabulary(terms) ``` -The add to your `configure.zcml`. +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: +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=_(u"Organiser"), - vocabulary=u"example.conference.organizers", + 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] package. -A resent and complete list can be found in the README of the package. +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`, -that lists the users of the portal. +For our example we could use `plone.app.vocabularies.Users`, which lists the users of the portal. -The `organizer` field now looks like: +The `organizer` field should now appear as shown. ```python organizer = schema.Choice( - title=_(u"Organiser"), - vocabulary=u"plone.app.vocabularies.Users", + 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]. -But, in a Plone context, you will more likely want to use [plone.formwidget.autocomplete], -which extends `z3c.formwidget.query` to provide friendlier user interface. +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], -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 `program.py`, we add the following import: +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` `value_type`, -we would use the `AutocompleteMultiFieldWidget` instead. -::: +```{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, recall, derives from `model.Schema` and is therefore processed for form hints at startup), +In the `IProgram` schema (which derives from `model.Schema`, and is therefore processed for form hints at startup), we then add the following: ```python @@ -346,21 +315,12 @@ from plone.autoform import directives directives.widget(organizer=AutocompleteFieldWidget) organizer = schema.Choice( - title=_(u'Organiser'), - vocabulary=u'plone.app.vocabularies.Users', + 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. +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 fall-back for non-JavaScript capable browsers. - -[plone.app.dexterity]: http://pypi.python.org/pypi/plone.app.dexterity -[plone.app.vocabularies]: http://pypi.python.org/pypi/plone.app.vocabularies -[plone.formwidget.autocomplete]: http://pypi.python.org/pypi/plone.formwidget.autocomplete -[plone.principalsource]: http://pypi.python.org/pypi/plone.principalsource -[z3c.form]: http://pypi.python.org/pypi/z3c.form -[z3c.formwidget.query]: http://pypi.python.org/pypi/z3c.formwidget.query -[zope.schema]: http://pypi.python.org/pypi/zope.schema +The widget also has a fall-back for non-JavaScript capable browsers. From 7bc8cc757d43ddcb517b981a49a7c3c96ccbe37c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 14:49:08 -0700 Subject: [PATCH 18/53] Tidy references.md --- plone.app.dexterity/advanced/references.md | 155 ++++++++------------- 1 file changed, 61 insertions(+), 94 deletions(-) diff --git a/plone.app.dexterity/advanced/references.md b/plone.app.dexterity/advanced/references.md index 6e3ce8392..08ba92296 100644 --- a/plone.app.dexterity/advanced/references.md +++ b/plone.app.dexterity/advanced/references.md @@ -1,101 +1,75 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**How to work with references between content objects** +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. +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], a Zope -2 integration layer for [zope.intid], to give each content item a unique -integer id. These are the basis for relationships maintained with the -[zc.relationship] package, which in turn is accessed via an API -provided by [z3c.relationfield], integrated into Zope 2 with -[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. +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] configured for the `RelationList` and -`RelationChoice` fields from `z3c.relationfield`. +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 `session.py`: - -``` -... +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=_(u"Presenter"), + 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. -::: +```{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` like: +To allow multiple items to be selected, we could have used a `RelationList` as shown: -``` +```python relatedItems = RelationList( - title=u"Related Items", + title="Related Items", default=[], - value_type=RelationChoice(title=_(u"Related"), + 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. +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 (e.g. you can’t use a dict to -specify a range or values for a field index). +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. +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 some that `plone.formwidget.contenttree` makes for you. For example: +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 @@ -105,49 +79,51 @@ you can use some that `plone.formwidget.contenttree` makes for you. For example: ``` -:::{note} -The pre-baked source binders were added in plone.formwidget.contenttree -1.0.7, which ships with Plone 4.3.2+. -::: +```{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 like -the autocomplete widget. The following line added to the interface will -make the presenter selection similar to the `organizer` selection widget -we showed in the previous section: +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: +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_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. +`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. +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 `templates/sessionview.pt`: +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`. -```html +```xml
``` + ## Back references -To retrieve back-reference (all objects pointing to particular object using specified attribute) you can't simply use `from_object` or `from_path`, because source object is stored in the relation without acquisition wrappers. -You should use `from_id` and `helper` method, which search the object in the `IntId` catalog: +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 @@ -166,20 +142,11 @@ def back_references(source_object, attribute_name): 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) + result.append(obj) return result ``` -Please note, this method does not check effective and expiration date or content language. - -Original issue: [http://code.google.com/p/dexterity/issues/detail?id=234](http://code.google.com/p/dexterity/issues/detail?id=234) - -[five.intid]: http://pypi.python.org/pypi/five.intid -[plone.app.relationfield]: http://pypi.python.org/pypi/plone.app.relationfield -[plone.formwidget.contenttree]: http://pypi.python.org/pypi/plone.formwidget.contenttree -[z3c.relationfield]: http://pypi.python.org/pypi/z3c.relationfield -[zc.relationship]: http://pypi.python.org/pypi/zc.relationship -[zope.intid]: http://pypi.python.org/pypi/zope.intid +This method does not check effective and expiration dates or content language. From 9c339ef8cfa668a64d3ce0bb313c2b7f0f2760d9 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 14:49:45 -0700 Subject: [PATCH 19/53] Strip Unicode specifier after dropping Python 2 support --- plone.app.dexterity/advanced/defaults.md | 4 ++-- .../advanced/files-and-images.md | 2 +- plone.app.dexterity/advanced/permissions.md | 6 +++--- .../rich-text-markup-transformations.md | 8 ++++---- plone.app.dexterity/advanced/validators.md | 2 +- .../webdav-and-other-file-representations.md | 12 +++++------ .../creating-and-registering-behaviors.md | 6 +++--- .../behaviors/providing-marker-interfaces.md | 18 ++++++++--------- .../behaviors/schema-only-behaviors.md | 6 +++--- .../reference/dexterity-xml.md | 2 +- .../reference/form-schema-hints.md | 20 +++++++++---------- .../reference/manipulating-content-objects.md | 4 ++-- 12 files changed, 45 insertions(+), 45 deletions(-) diff --git a/plone.app.dexterity/advanced/defaults.md b/plone.app.dexterity/advanced/defaults.md index 314c69a74..c5c98e546 100644 --- a/plone.app.dexterity/advanced/defaults.md +++ b/plone.app.dexterity/advanced/defaults.md @@ -33,13 +33,13 @@ We also need to modify `IProgram` so the `start` and `end` fields use these func class IProgram(model.Schema): start = schema.Datetime( - title=_(u"Start date"), + title=_("Start date"), required=False, defaultFactory=startDefaultValue, ) end = schema.Datetime( - title=_(u"End date"), + title=_("End date"), required=False, defaultFactory=endDefaultValue, ) diff --git a/plone.app.dexterity/advanced/files-and-images.md b/plone.app.dexterity/advanced/files-and-images.md index 12ef41677..19302f47f 100644 --- a/plone.app.dexterity/advanced/files-and-images.md +++ b/plone.app.dexterity/advanced/files-and-images.md @@ -65,7 +65,7 @@ class IPresenter(model.Schema): ... picture = NamedBlobImage( - title=_(u"Please upload an image"), + title=_("Please upload an image"), required=False, ) ``` diff --git a/plone.app.dexterity/advanced/permissions.md b/plone.app.dexterity/advanced/permissions.md index 8c3d9c8cb..7e20053d3 100644 --- a/plone.app.dexterity/advanced/permissions.md +++ b/plone.app.dexterity/advanced/permissions.md @@ -129,7 +129,7 @@ class IExampleProtectedInformation(model): read_permission(info='cmf.ManagePortal') write_permission(info='cmf.ManagePortal') info = schema.Text( - title=_(u"Information"), + title=_("Information"), ) ``` @@ -142,7 +142,7 @@ First, we add this to the `IProgram` schema in `program.py`: ``` form.widget(tracks=TextLinesFieldWidget) tracks = schema.List( - title=_(u"Tracks"), + title=_("Tracks"), required=True, default=[], value_type=schema.TextLine(), @@ -192,7 +192,7 @@ protect it with the relevant write permission: ``` write_permission(track='example.conference.ModifyTrack') track = schema.Choice( - title=_(u"Track"), + title=_("Track"), source=possibleTracks, required=False, ) diff --git a/plone.app.dexterity/advanced/rich-text-markup-transformations.md b/plone.app.dexterity/advanced/rich-text-markup-transformations.md index fbae0beb0..17c3137d4 100644 --- a/plone.app.dexterity/advanced/rich-text-markup-transformations.md +++ b/plone.app.dexterity/advanced/rich-text-markup-transformations.md @@ -32,7 +32,7 @@ from plone.app.z3cform.wysiwyg import WysiwygFieldWidget class ITestSchema(model.Schema): form.widget('body', WysiwygFieldWidget) - body = schema.Text(title=u"Body text") + body = schema.Text(title="Body text") ``` (richtext-label)= @@ -47,7 +47,7 @@ from plone.supermodel import model class ITestSchema(model.Schema): - body = RichText(title=u"Body text") + body = RichText(title="Body text") ``` The `RichText` field constructor can take the following arguments in @@ -90,7 +90,7 @@ And this class ITestSchema(model.Schema): body = RichText( - title=u"Body text", + title="Body text", default_mime_type='text/x-rst', output_mime_type='text/x-html', allowed_mime_types=('text/x-rst', 'text/structured',), @@ -152,7 +152,7 @@ type from the field instance.): from plone.app.textfield.value import RichTextValue ... -context.body = RichTextValue(u"Some input text", 'text/plain', 'text/html') +context.body = RichTextValue("Some input text", 'text/plain', 'text/html') ``` Of course, the standard widget used for a `RichText` field will diff --git a/plone.app.dexterity/advanced/validators.md b/plone.app.dexterity/advanced/validators.md index 9a6166e6c..be134c604 100644 --- a/plone.app.dexterity/advanced/validators.md +++ b/plone.app.dexterity/advanced/validators.md @@ -49,7 +49,7 @@ The constraint function does not have access to the context, but if you need to To use the constraint, pass the function as the `constraint` argument to the field constructor, for example: ```python -my_field = schema.TextLine(title=_(u"My field"), constraint=checkForMagic) +my_field = schema.TextLine(title=_("My field"), constraint=checkForMagic) ``` Constraints are easy to write, but do not necessarily produce very friendly error messages. diff --git a/plone.app.dexterity/advanced/webdav-and-other-file-representations.md b/plone.app.dexterity/advanced/webdav-and-other-file-representations.md index ee574a0e5..1debd37f5 100644 --- a/plone.app.dexterity/advanced/webdav-and-other-file-representations.md +++ b/plone.app.dexterity/advanced/webdav-and-other-file-representations.md @@ -67,30 +67,30 @@ class ISession(model.Schema): """ title = schema.TextLine( - title=_(u"Title"), - description=_(u"Session title"), + title=_("Title"), + description=_("Session title"), ) description = schema.Text( - title=_(u"Session summary"), + title=_("Session summary"), ) directives.primary('details') details = RichText( - title=_(u"Session details"), + title=_("Session details"), required=False ) form.widget(presenter=AutocompleteFieldWidget) presenter = RelationChoice( - title=_(u"Presenter"), + title=_("Presenter"), source=ObjPathSourceBinder(object_provides=IPresenter.__identifier__), required=False, ) form.write_permission(track='example.conference.ModifyTrack') track = schema.Choice( - title=_(u"Track"), + title=_("Track"), source=possibleTracks, required=False, ) diff --git a/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md b/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md index d85334e9c..e63010fd5 100644 --- a/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md +++ b/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md @@ -87,13 +87,13 @@ class ITags(model.Schema): directives.fieldset( 'categorization', - label=_(u'Categorization'), + label=_('Categorization'), fields=('tags',), ) tags = Tags( - title=_(u"Tags"), - description=_(u"Applicable tags"), + title=_("Tags"), + description=_("Applicable tags"), required=False, allow_uncommon=True, ) diff --git a/plone.app.dexterity/behaviors/providing-marker-interfaces.md b/plone.app.dexterity/behaviors/providing-marker-interfaces.md index c6a271b91..204691836 100644 --- a/plone.app.dexterity/behaviors/providing-marker-interfaces.md +++ b/plone.app.dexterity/behaviors/providing-marker-interfaces.md @@ -107,7 +107,7 @@ class IReviewers(model.Schema): directives.fieldset( 'ownership', - label=_(u'Ownership'), + label=_('Ownership'), fields=( 'official_reviewers', 'unofficial_reviewers' @@ -117,13 +117,13 @@ class IReviewers(model.Schema): directives.widget(official_reviewers=AutocompleteMultiFieldWidget) directives.write_permission(official_reviewers='iz.EditOfficialReviewers') official_reviewers = schema.Tuple( - title=_(u'Official reviewers'), + title=_('Official reviewers'), description=_( - u'People or groups who may review this item in an official ' - u'capacity.' + 'People or groups who may review this item in an official ' + 'capacity.' ), value_type=schema.Choice( - title=_(u"Principal"), + title=_("Principal"), source="plone.principalsource.Principals" ), required=False, @@ -133,13 +133,13 @@ class IReviewers(model.Schema): directives.widget(unofficial_reviewers=AutocompleteMultiFieldWidget) directives.write_permission(unofficial_reviewers='iz.EditUnofficialReviewers') unofficial_reviewers = schema.Tuple( - title=_(u'Unofficial reviewers'), + title=_('Unofficial reviewers'), description=_( - u'People or groups who may review this item in a supplementary ' - u'capacity' + 'People or groups who may review this item in a supplementary ' + 'capacity' ), value_type=schema.Choice( - title=_(u"Principal"), + title=_("Principal"), source="plone.principalsource.Principals" ), required=False, diff --git a/plone.app.dexterity/behaviors/schema-only-behaviors.md b/plone.app.dexterity/behaviors/schema-only-behaviors.md index cd1999a9c..1078c04a7 100644 --- a/plone.app.dexterity/behaviors/schema-only-behaviors.md +++ b/plone.app.dexterity/behaviors/schema-only-behaviors.md @@ -74,13 +74,13 @@ class IRelatedItems(model.Schema): """Behavior interface to make a type support related items. """ - form.fieldset('categorization', label=u"Categorization", + form.fieldset('categorization', label="Categorization", fields=['relatedItems']) relatedItems = RelationList( - title=u"Related Items", + title="Related Items", default=[], - value_type=RelationChoice(title=u"Related", + value_type=RelationChoice(title="Related", source=ObjPathSourceBinder()), required=False, ) diff --git a/plone.app.dexterity/reference/dexterity-xml.md b/plone.app.dexterity/reference/dexterity-xml.md index 6166753a4..af23a1aee 100644 --- a/plone.app.dexterity/reference/dexterity-xml.md +++ b/plone.app.dexterity/reference/dexterity-xml.md @@ -280,7 +280,7 @@ Sample Python for the validator factory: ```python @provider(IDefaultFactory) def dummy_defaultFactory(): - return u'something' + return 'something' ``` For a callable using context: diff --git a/plone.app.dexterity/reference/form-schema-hints.md b/plone.app.dexterity/reference/form-schema-hints.md index 496457f89..66e5d6b61 100644 --- a/plone.app.dexterity/reference/form-schema-hints.md +++ b/plone.app.dexterity/reference/form-schema-hints.md @@ -28,7 +28,7 @@ from zope import schema class ISampleSchema(model.Schema): - title = schema.TextLine(title=u'Title') + title = schema.TextLine(title='Title') directives.omitted('additionalInfo') additionalInfo = schema.Bytes() @@ -80,7 +80,7 @@ class ISampleSchema(model.Schema): # fieldset has already been defined. fieldset('extra', - label=u'Extra information', + label='Extra information', fields=['footer', 'dummy'] ) @@ -90,16 +90,16 @@ class ISampleSchema(model.Schema): directives.widget(body='plone.app.z3cform.wysiwyg.WysiwygFieldWidget') primary('body') body = schema.Text( - title=u'Body text', + title='Body text', required=False, - default=u'Body text goes here' + default='Body text goes here' ) # The widget can also be specified as an object directives.widget(footer=WysiwygFieldWidget) footer = schema.Text( - title=u'Footer text', + title='Footer text', required=False ) @@ -108,15 +108,15 @@ class ISampleSchema(model.Schema): directives.omitted('dummy') dummy = schema.Text( - title=u'Dummy' + title='Dummy' ) # A field in 'hidden' mode directives.mode(secret='hidden') secret = schema.TextLine( - title=u'Secret', - default=u'Secret stuff' + title='Secret', + default='Secret stuff' ) # This field is moved before the 'description' field of the standard @@ -124,7 +124,7 @@ class ISampleSchema(model.Schema): directives.order_before(importantNote='IDublinCore.description') importantNote = schema.TextLine( - title=u'Important note', + title='Important note', ) ``` @@ -153,7 +153,7 @@ class ISampleSchema(model.Schema): directives.read_permission(reviewNotes='cmf.ReviewPortalContent') directives.write_permission(reviewNotes='cmf.ReviewPortalContent') reviewNotes = schema.Text( - title=u'Review notes', + title='Review notes', required=False, ) ``` diff --git a/plone.app.dexterity/reference/manipulating-content-objects.md b/plone.app.dexterity/reference/manipulating-content-objects.md index 3548d1d30..c6aacaeda 100644 --- a/plone.app.dexterity/reference/manipulating-content-objects.md +++ b/plone.app.dexterity/reference/manipulating-content-objects.md @@ -55,7 +55,7 @@ object. It is mostly useful in tests: ```python from plone.dexterity.utils import createContent -context = createContent('example.type', title=u"Foo") +context = createContent('example.type', title="Foo") ``` Any keyword arguments are used to set properties on the new instance @@ -107,7 +107,7 @@ You can also both create and add an object in one call: ```python from plone.dexterity.utils import createContentInContainer -createContentInContainer(folder, 'example.type', title=u"Foo") +createContentInContainer(folder, 'example.type', title="Foo") ``` Again, you can pass *checkConstraints=False* to bypass folder From bf3c1fc4e39ff4222ae96cfa42ef1df3ae4702dd Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 22:02:17 -0700 Subject: [PATCH 20/53] Tidy rich-text-markup-transformations.md --- .../rich-text-markup-transformations.md | 188 +++++++----------- 1 file changed, 77 insertions(+), 111 deletions(-) diff --git a/plone.app.dexterity/advanced/rich-text-markup-transformations.md b/plone.app.dexterity/advanced/rich-text-markup-transformations.md index 17c3137d4..15c58e99c 100644 --- a/plone.app.dexterity/advanced/rich-text-markup-transformations.md +++ b/plone.app.dexterity/advanced/rich-text-markup-transformations.md @@ -1,29 +1,24 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 +# Rich text, markup, and transformations -**How to store markup (such as HTML or reStructuredText) and render it with a transformation** +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 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. +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 this: +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 @@ -37,11 +32,10 @@ class ITestSchema(model.Schema): (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] package: +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 @@ -50,28 +44,26 @@ 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: +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. +`default_mime_type` +: A string representing the default MIME type of the input markup. + This defaults to `text/html`. -Also note: 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). +`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. -Below is an example of a field allow StructuredText and -reStructuredText, transformed to HTML by default: +`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 @@ -98,130 +90,104 @@ class ITestSchema(model.Schema): ) ``` + ## 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: +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; +: A Unicode string with the original input markup. `mimeType` - -: the MIME type of the original markup, e.g. `text/html` or - `text/structured`; +: 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; +: 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; +: 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; +: 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). -: 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 optimised for the case where -the transformed output will be read frequently (i.e. on the view screen -of the content object) and the raw value will be read infrequently (i.e. -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 should 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.): +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. +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 are using a -`DisplayForm`, the display widget for the `RichText` field will render -the transformed output markup automatically. If you are writing TAL -manually, you may try something like this: +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. -```html +```xml
``` -This, however, will render a string like: +This, however, will render a string as follows. -``` +```html RichTextValue object. (Did you mean .raw or .output?) ``` The correct syntax is: -```html +```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. +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. (Should you need to write your own transforms, -take a look at [this tutorial].) This is abstracted behind an -`ITransformer` adapter to allow alternative implementations. +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: +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. +The `__call__()` method of the `ITransformer` adapter takes a `RichTextValue` object and an output MIME type as parameters. -If you are writing a page template, there is an even more convenient -syntax: +If you write a page template, there is an even more convenient syntax. -```html +```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. +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. -::: - -[plone.app.textfield]: http://pypi.python.org/pypi/plone.app.textfield -[this tutorial]: http://plone.org/documentation/kb/portal-transforms +```{note} +Unlike the `output` property, the value is not cached, and so will be calculated each time the page is rendered. +``` From c3799cd4e3f427425bcca10371cfc38b6b859801 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 22:13:11 -0700 Subject: [PATCH 21/53] Tidy files-and-images.md --- .../advanced/files-and-images.md | 134 ++++++++---------- 1 file changed, 60 insertions(+), 74 deletions(-) diff --git a/plone.app.dexterity/advanced/files-and-images.md b/plone.app.dexterity/advanced/files-and-images.md index 19302f47f..e7b0d1b25 100644 --- a/plone.app.dexterity/advanced/files-and-images.md +++ b/plone.app.dexterity/advanced/files-and-images.md @@ -1,68 +1,64 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Working 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] and -[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 -like `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 directly from `plone.namedfile`: +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', + "plone.namedfile", + "plone.formwidget.namedfile", ], ``` -:::{note} -Again, we do not need separate `` lines in -`configure.zcml` for these new dependencies, because we use -``. -::: +```{note} +Again, we do not need separate `` lines in `configure.zcml` for these new dependencies, because we use ``. +``` -For the sake of illustration, we will add an image of the -speaker to the `Presenter` type. In `presenter.py`, we add: +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"), @@ -70,14 +66,13 @@ class IPresenter(model.Schema): ) ``` -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). +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 `presenter_templates/view.pt`, we add this block of TAL: +In {file}`presenter_templates/view.pt`, we add the following block of TAL. -``` +```xml
``` -where `scales` is large, preview, mini, thumb, tile, icon, or a custom scale. -This code generates a full tag, including height and width attributes and alt and title based on the context title. -To generate just a URL, use code like: +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, e.g.: +For file fields, you can construct a download URL in a similar way, using an `` tag. -``` +```xml ``` - -[extra]: http://peak.telecommunity.com/DevCenter/setuptools#declaring-extras-optional-features-with-their-own-dependencies -[plone.formwidget.namedfile]: http://pypi.python.org/pypi/plone.formwidget.namedfile -[plone.namedfile]: http://pypi.python.org/pypi/plone.namedfile -[z3c.blobfile]: http://pypi.python.org/pypi/z3c.blobfile From e502f9f90cc6ce85549c026c338654e8209dd1ac Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 22:22:00 -0700 Subject: [PATCH 22/53] tidy static-resources.md --- .../advanced/static-resources.md | 119 ++++++++---------- 1 file changed, 49 insertions(+), 70 deletions(-) diff --git a/plone.app.dexterity/advanced/static-resources.md b/plone.app.dexterity/advanced/static-resources.md index 0ce62cedf..c9080a888 100644 --- a/plone.app.dexterity/advanced/static-resources.md +++ b/plone.app.dexterity/advanced/static-resources.md @@ -1,34 +1,32 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Adding images and stylesheets** +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. -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. +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. +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: +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 `conference.css`, it will be accessible on a URL -like `http:///site/++resource++example.conference/conference.css.` -The resource name is the same as the package name wherein the `resources` -directory appears. +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. +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 -`presenter_templates/view.pt`: +For example, we could add the following near the top of {file}`presenter_templates/view.pt`. -```html +```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 -`cssregistry.xml` or `jsregistry.xml` import steps in the -`profiles/default` directory. For example, an import step to add the -`conference.css` file site-wide may involve a `cssregistry.xml` file -that looks like this: +```{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 @@ -103,8 +85,7 @@ that looks like this:
``` -Similarly, a JavaScript resource could be imported with a -`jsregistry.xml` like: +Similarly, a JavaScript resource could be imported with a {file}`jsregistry.xml` file such as the following. ```xml @@ -115,26 +96,24 @@ Similarly, a JavaScript resource could be imported with a
``` + ## 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 -like this: +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. -```html +```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 -`profiles/default/types/example.conference.presenter.xml`, we can use -the following line, presuming we have a `presenter.gif` in the `example.conference` resource -directory: +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 From d47f17b47e11d7a6e1c277bed7ef762f72ed8fe2 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 22:22:15 -0700 Subject: [PATCH 23/53] US English spelling --- plone.app.dexterity/advanced/{behaviours.md => behaviors.md} | 0 plone.app.dexterity/advanced/index.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename plone.app.dexterity/advanced/{behaviours.md => behaviors.md} (100%) diff --git a/plone.app.dexterity/advanced/behaviours.md b/plone.app.dexterity/advanced/behaviors.md similarity index 100% rename from plone.app.dexterity/advanced/behaviours.md rename to plone.app.dexterity/advanced/behaviors.md diff --git a/plone.app.dexterity/advanced/index.md b/plone.app.dexterity/advanced/index.md index fc8a21291..bb2b6d043 100644 --- a/plone.app.dexterity/advanced/index.md +++ b/plone.app.dexterity/advanced/index.md @@ -21,7 +21,7 @@ references rich-text-markup-transformations files-and-images static-resources -behaviours +behaviors event-handlers permissions workflow From aadcec3e6131fdd27f2a41f024e4156e54ec55c6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 15 Apr 2023 22:29:43 -0700 Subject: [PATCH 24/53] Tidy behaviors.md, add label and reference to Built-in behaviors --- docs/backend/behaviors.md | 2 + plone.app.dexterity/advanced/behaviors.md | 55 +++++++++-------------- 2 files changed, 22 insertions(+), 35 deletions(-) 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/plone.app.dexterity/advanced/behaviors.md b/plone.app.dexterity/advanced/behaviors.md index a5bc2831f..7961db893 100644 --- a/plone.app.dexterity/advanced/behaviors.md +++ b/plone.app.dexterity/advanced/behaviors.md @@ -1,36 +1,28 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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" --- -# Using behaviors +# Behaviors -**Finding and adding behaviors** +This chapter describes how to find and add behaviors. -Dexterity introduces the concept of *behaviors* – re-usable bundles of -functionality and/or form fields which can be turned on or off on a -per-type basis. +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. +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 in this manual, but we will show -how to enable behaviors on a type. Writing behaviors is covered in the -[Behaviors manual](http://docs.plone.org/external/plone.app.dexterity/docs/behaviors/index.html). +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: +In fact, we've already seen one standard behavior applied to our example types, registered in the FTI and imported using GenericSetup. ```xml @@ -38,17 +30,10 @@ and imported using GenericSetup: ``` -Other behaviors are added in the same way, by listing additional -behavior interfaces as elements of the `behaviors` property. +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. +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]. For a list -of standard behaviors that ship with Dexterity, see the reference at the -end of this manual. - -[plone.app.dexterity]: http://pypi.python.org/pypi/plone.app.dexterity +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 From dcdd699489d703c8b91e2c773ad762beca2c312c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 16 Apr 2023 02:07:56 -0700 Subject: [PATCH 25/53] Tidy event-handlers.md --- .../advanced/event-handlers.md | 97 +++++++++---------- 1 file changed, 44 insertions(+), 53 deletions(-) diff --git a/plone.app.dexterity/advanced/event-handlers.md b/plone.app.dexterity/advanced/event-handlers.md index 3d0e2a2c9..0ae5d8da8 100644 --- a/plone.app.dexterity/advanced/event-handlers.md +++ b/plone.app.dexterity/advanced/event-handlers.md @@ -1,108 +1,101 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Adding custom event handlers for your type** +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. +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] package) all registered event handlers will be called. -This happens for example from the `save` action of an add form, on move or delete of content-objects. -There is no guarantee of which order the event handlers will be called in, however. +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. +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. +: 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`. +: 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). +: 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. +: 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`. +: 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). +: 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. +: 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. +: 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 send these users an email. +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 `presenter.py`: +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: +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') + 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( + 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) + email = user_info.get("email", None) if email is not None: api.portal.send_email( recipient=email, @@ -112,11 +105,11 @@ def notifyUser(presenter, event): ) ``` -And register it in ZCML: +And register it in ZCML. -- First argument to `for` is an interface describing the object type. -- 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. +- 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 Date: Sun, 16 Apr 2023 02:20:13 -0700 Subject: [PATCH 26/53] Tidy permissions.md --- plone.app.dexterity/advanced/permissions.md | 150 ++++++++------------ 1 file changed, 61 insertions(+), 89 deletions(-) diff --git a/plone.app.dexterity/advanced/permissions.md b/plone.app.dexterity/advanced/permissions.md index 7e20053d3..e181d7983 100644 --- a/plone.app.dexterity/advanced/permissions.md +++ b/plone.app.dexterity/advanced/permissions.md @@ -1,56 +1,45 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Setting up add permissions, view permissions and field view/edit permissions** - -Plone’s security system is based on the concept of -*permissions* protecting *operations* -(like accessing a view, -viewing a field, -modifying a field, -or adding a type of content) -that are granted to *roles*, -which in turn are granted to *users* and/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. +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] in the Plone documentation. +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. +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. +```{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 `session.py`, we update the `View` class with the following: +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): @@ -59,9 +48,9 @@ class View(BrowserView): return checkPermission('cmf.RequestReview', self.context) ``` -And in the `session_templates/view.pt` template, we add: +And in the {file}`session_templates/view.pt` template, we add the following. -```html +```xml
@@ -69,57 +58,48 @@ And in the `session_templates/view.pt` template, we add:
``` + ## 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. +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 -`profiles/default/example.conference.session.xml`: +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 (like viewlets or portlets) -can be protected by permissions, either using the `permission` attribute -on ZCML statements like: +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 -(e.g. scripts or templates created through the web) -and URL traversal, -and 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. +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] package. +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 defintion see {ref}`Dexterity XML: security attributes `. +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). -Simple example protecting a field to be readable for Site Administrators only: +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 @@ -133,13 +113,12 @@ class IExampleProtectedInformation(model): ) ``` -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. +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 `program.py`: +First, we add this to the `IProgram` schema in {file}`program.py`. -``` +```python form.widget(tracks=TextLinesFieldWidget) tracks = schema.List( title=_("Tracks"), @@ -149,21 +128,21 @@ tracks = schema.List( ) ``` -The `TextLinesFieldWidget` is used to edit a list of text lines in a -text area. It is imported as: +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 `session.py`: +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): @@ -180,16 +159,13 @@ def possibleTracks(context): 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) -and uses its `tracks` variable as the vocabulary. +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: +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"), @@ -198,9 +174,5 @@ track = schema.Choice( ) ``` -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. - -[plone.autoform]: http://pypi.python.org/pypi/plone.autoform -[security section]: http://docs.plone.org/develop/plone/security/index.html +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. From 423414865256599262fccbfef4b30365f3a2e575 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 16 Apr 2023 02:46:33 -0700 Subject: [PATCH 27/53] Tidy workflow.md --- plone.app.dexterity/advanced/workflow.md | 365 ++++++++--------------- 1 file changed, 119 insertions(+), 246 deletions(-) diff --git a/plone.app.dexterity/advanced/workflow.md b/plone.app.dexterity/advanced/workflow.md index ab17f1e46..9163ea7b1 100644 --- a/plone.app.dexterity/advanced/workflow.md +++ b/plone.app.dexterity/advanced/workflow.md @@ -1,258 +1,155 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Controlling security with workflow** +This chapter describes how to control security with workflow. -Workflow is used in Plone for three distinct, but overlapping purposes: +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. +- 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. +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. -:::{note} -There is nothing Dexterity-specific in this section. -Everything here applies equally well to content objects -created with Archetypes or using CMF directly. -::: ## A DCWorkflow refresher -What follows is a fairly detailed description of [DCWorkflow], -originally posted [here]. -You may find some of this a little detailed on first reading, -so feel free to skip to the specifics later on. +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 -like {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 +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] expressions that are invoked -to calculate the current value at the point of transition. +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*. +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 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, -e.g. 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. +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. +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 anonymous!). +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** +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. +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 -(i.e. before the object enters the target state) -or just after it has been completed (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. +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](http://docs.zope.org/zope2/zope2book/BasicObject.html#logic-objects-script-python-objects-and-external-methods) - in the Zope 2 Book. + 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. +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 specialisation of `IReadSequence`, i.e. a tuple). -The default obviously looks at the mappings in the `portal_workflow` tool, -but it is possible to override the mapping, -e.g. by using a custom adapter registered for some marker interface, -which in turn could be provided by a type-specific behavior. +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 UI 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. +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*. +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. +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. +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] 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. +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 `setup.py`, we have: +In {file}`setup.py`, we have the following. -``` +```python install_requires=[ - ... 'collective.wtf', ], ``` -:::{note} -As before, the `` line in `configure.zcml` -takes care of configuring the package for us. -::: +```{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 `workflows.xml` file in `profiles/default` -which maps types to workflows. +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 `profiles/default/workflows.xml` looks like -this: +The workflow mapping in {file}`profiles/default/workflows.xml` is shown below. ```xml @@ -265,20 +162,16 @@ this:
``` -The CSV file itself is found in -`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]. 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] as a -starting point. -::: +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" @@ -342,50 +235,30 @@ starting point. ``` 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. -::: +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. +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. - -[collective.wtf]: http://pypi.python.org/pypi/collective.wtf -[dcworkflow]: http://pypi.python.org/pypi/Products.DCWorkflow -[example.conference source code]: http://svn.plone.org/svn/collective/example.conference/trunk/example/conference/profiles/default/workflow_csv -[here]: http://www.martinaspeli.net/articles/dcworkflows-hidden-gems -[tales]: http://docs.zope.org/zope2/zope2book/AppendixC.html#tales-overview -[this template]: ../Workflow%20template.ods +- 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. From 60536db743b5a9c21ca884f0dce9fc74ba516a54 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 16 Apr 2023 14:48:35 -0700 Subject: [PATCH 28/53] Tidy catalog-indexing-strategies.md --- .../advanced/catalog-indexing-strategies.md | 285 ++++++++---------- 1 file changed, 132 insertions(+), 153 deletions(-) diff --git a/plone.app.dexterity/advanced/catalog-indexing-strategies.md b/plone.app.dexterity/advanced/catalog-indexing-strategies.md index 5b36d6722..46be0ae53 100644 --- a/plone.app.dexterity/advanced/catalog-indexing-strategies.md +++ b/plone.app.dexterity/advanced/catalog-indexing-strategies.md @@ -1,112 +1,117 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 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. +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. -So, 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. +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. +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} -Note that 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 like Title and Description are no longer automatically handled. -::: +```{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 the [/backend/indexing](https://6-dev-docs.plone.org/backend/indexing) package documentation for details and for information on how to use it via Python schema. +See {doc}`/backend/indexing` documentation for details and for information on how to use it via Python schema. + ## Creating and using custom indexes -**How to create custom catalog 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 optimised for searching. -In Plone, there’s a ZCatalog instance called `portal_catalog`. +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. +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} -Dexterity objects are more lightweight than Archetypes objects. -This means that loading objects into memory is not quite as undesirable as is sometimes assumed. -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. +```{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. +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: +Indexes come in various types. The most common ones are the following. `FieldIndex` - -: the most common type, used to index a single value. +: 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. +: 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! +: 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. +: Used mainly for the effective date range. `ZCTextIndex` - -: used mainly for the `SearchableText` index. - This is the index used for full-text search. +: 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. -: 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, the catalog will by default 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. +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} +```{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/methods with names corresponding to indexes, the parent object’s value will be indexed for all children as well. -::: +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 (`parts/omelette/Products/CMFPlone/profiles/default/catalog.xml`). +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 -`profiles/default/catalog.xml`, we have: +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 @@ -122,22 +127,22 @@ 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 can sometimes 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. +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] to help make it easier to write custom indexers: -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. +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 `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!) +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 @@ -162,7 +167,7 @@ def tracksIndexer(obj): return obj.tracks ``` -And we need to register the indexers in ZCML: +And we need to register the indexers in ZCML. ```xml @@ -171,20 +176,21 @@ And we need to register the indexers in ZCML: ``` 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 finalise the registration. -Crucially, this is where the indexer’s `name` is defined. +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 are creating 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. -::: +```{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. +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: +The pattern is always the same, as shown below. ```python from plone import api @@ -200,128 +206,101 @@ for brain in results: ``` 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. +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. +: The object's title. `Description` - -: the object’s description. +: The object's description. `path` - -: the object’s path. The argument is a string like `/foo/bar`. - To get the path of an object (e.g. a parent folder), do - `'/'.join(folder.getPhysicalPath())`. - Searching for an object’s path will return the object and any children. - To depth-limit the search, e.g. to get only those 1 level deep, - use a compound query, e.g. - `path={'query': '/'.join(folder.getPhysicalPath()), 'depth': 1}`. - If a depth is specified, the object at the given path is not returned - (but any children within the depth limit are). +: 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__`. +: 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`. +: 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 like `AND` and `OR` in the search string. +: 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. +: The username of the creator of a content item. `Subject` - -: a `KeywordIndex` of object keywords. +: A `KeywordIndex` of object keywords. `review_state` +: An object's workflow 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 optimisations. - Use a list slice on the catalog search results to be absolutely sure that you have got the maximum number of results, e.g. - `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: - -*Creator* - -: the user who created the content object. - -*Date* +In addition, the search results can be sorted based on any `FieldIndex`, `KeywordIndex`, or `DateIndex` using the following keyword arguments. -: the publication date or creation date, whichever is later. +- 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. -*Title* +Some of the more commonly used metadata columns are the following. -: the object’s title. - -*Description* - -: the object’s description. +`Creator` +: The user who created the content object. -*getId* +`Date` +: The publication date or creation date, whichever is later. -: the object’s id (note that this is an attribute, not a function). +`Title` +: The object's title. -*review_state* +`Description` +: The object's description. -: the object’s workflow state. +`getId` +: The object's ID. + Note that this is an attribute, not a function. -*portal_type* +`review_state` +: The object's workflow state. -: the object’s portal type. +`portal_type` +: The object's portal type. For more information about catalog indexes and searching, see the -[ZCatalog chapter in the Zope 2 book]. +[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 index-able, we need to create the index itself. +Now that the fields are indexible, we need to create the index itself. -- Go to the Zope Management Interface -- Go on 'portal_catalog' -- Click 'Indexes' tab -- There's a drop down menu to the top right to let you choose what type of index to add - if you are using a plain text string field you would select 'FieldIndex' -- As the 'id' put in the programmatical name of your Dexterity type field that you want to index -- Hit OK, tick your new index and click 'Reindex' +- 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 {doc}`documentation ` for further information - -[plone.indexer]: http://pypi.python.org/pypi/plone.indexer -[zcatalog chapter in the zope 2 book]: https://zope.readthedocs.io/en/latest/zopebook/SearchingZCatalog.html +See the [documentation on Indexes and Metadata](https://5.docs.plone.org/develop/plone/searching_and_indexing/indexing) for further information. From f9e0ab949a96ce8b64f5082b127d5b0f21286a1e Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 17 Apr 2023 04:49:09 -0700 Subject: [PATCH 29/53] Tidy custom-add-and-edit-forms.md --- .../advanced/custom-add-and-edit-forms.md | 233 +++++++----------- 1 file changed, 91 insertions(+), 142 deletions(-) diff --git a/plone.app.dexterity/advanced/custom-add-and-edit-forms.md b/plone.app.dexterity/advanced/custom-add-and-edit-forms.md index 7e7df6908..bbf56be3a 100644 --- a/plone.app.dexterity/advanced/custom-add-and-edit-forms.md +++ b/plone.app.dexterity/advanced/custom-add-and-edit-forms.md @@ -1,51 +1,38 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Using \`z3c.form\`\_ to build custom 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. +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] library to build its forms, via the -[plone.z3cform] integration package. +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], 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. +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/). +``` -:::{note} -If you want to build standalone forms not related to content objects, -see the [z3c.form] documentation. -::: ## 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. +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. +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: +To create an edit form that is identical to the default, we could do the following. ```python from plone.dexterity.browser import edit @@ -54,7 +41,7 @@ class EditForm(edit.DefaultEditForm): pass ``` -and register it in configure.zcml: +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: - -- 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 - different 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] and [z3c.form] documentation - 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. +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. +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 realise 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 re-use existing -classes and factories. +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++`. +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 this: - -- 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 like `/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 initialises 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 initialised 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. +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. +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. +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 look like this: +A custom form replicating the default would be the following. ```python from plone.dexterity.browser import add @@ -175,7 +137,7 @@ class AddView(add.DefaultAddView): form = AddForm ``` -and be registered in ZCML like this: +And be registered in ZCML as follows. ```xml Date: Mon, 17 Apr 2023 05:29:54 -0700 Subject: [PATCH 30/53] Tidy custom-content-classes.md --- .../advanced/custom-content-classes.md | 78 +++++++------------ 1 file changed, 26 insertions(+), 52 deletions(-) diff --git a/plone.app.dexterity/advanced/custom-content-classes.md b/plone.app.dexterity/advanced/custom-content-classes.md index 95edb1074..145e542eb 100644 --- a/plone.app.dexterity/advanced/custom-content-classes.md +++ b/plone.app.dexterity/advanced/custom-content-classes.md @@ -1,92 +1,66 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Adding a custom implementation** +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] module, -and represent container (folder) and item (non-folder) types, respectively. +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. +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. -Creating a custom class is simple: simply derive from one of the -standard ones, e.g.: +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: +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, e.g. +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. +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. +``` -:::{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]). -However, you need to be careful that this factory performs all necessary -initialisation, 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 +- 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__` and/or - `isPrincipiaFolderish` properties, and possibly the `__getattr__()` - and `__getitem__()` methods, - causing problems with the dynamic schemata and/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. - -[plone.dexterity.content]: http://pypi.python.org/pypi/plone.dexterity.content -[plone.dexterity.factory]: http://pypi.python.org/pypi/plone.dexterity.factory +- 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. From 03606844832db4489b13c4e3efeac9cad94bd405 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 18 Apr 2023 07:19:27 -0700 Subject: [PATCH 31/53] Tidy webdav-and-other-file-representations.md --- .../webdav-and-other-file-representations.md | 528 +++++++----------- 1 file changed, 197 insertions(+), 331 deletions(-) diff --git a/plone.app.dexterity/advanced/webdav-and-other-file-representations.md b/plone.app.dexterity/advanced/webdav-and-other-file-representations.md index 1debd37f5..b1902a81b 100644 --- a/plone.app.dexterity/advanced/webdav-and-other-file-representations.md +++ b/plone.app.dexterity/advanced/webdav-and-other-file-representations.md @@ -1,62 +1,57 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Adding support for WebDAV and accessing and modifying a -content object using file-like operations** +```{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). -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. In addition, WebDAV powers the -[External Editor] product, which allows users to launch a desktop -program from within Plone to edit a content object. +> 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. +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] for details. -When Zope is started, you should now be able to mount it as a WebDAV -server on the given port. +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 such as Novell NetDrive. - -[^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 Mac OS X, the Finder claims to support WebDAV, but the implementation -is so flakey that it is just as likely to crash Mac OS X as it is to let -you browse files and folders. Use a dedicated WebDAV client instead, -such as [Cyberduck]. - -## Default WebDAV behaviour - -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. +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]. For example: +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 @@ -75,7 +70,7 @@ class ISession(model.Schema): title=_("Session summary"), ) - directives.primary('details') + directives.primary("details") details = RichText( title=_("Session details"), required=False @@ -88,7 +83,7 @@ class ISession(model.Schema): required=False, ) - form.write_permission(track='example.conference.ModifyTrack') + form.write_permission(track="example.conference.ModifyTrack") track = schema.Choice( title=_("Track"), source=possibleTracks, @@ -96,12 +91,11 @@ class ISession(model.Schema): ) ``` -This will actually apply the `IPrimaryField` marker interface from the -[plone.rfc822] package to the given field(s). +This will actually apply the `IPrimaryField` marker interface from the [`plone.rfc822`](https://pypi.org/project/plone.rfc822/) package to the given fields. -A WebDAV download of this content item will by default look like this: +By default a WebDAV download of this content item will look like the following. -``` +```text title: Test session description: First session presenter: 713399904 @@ -114,69 +108,46 @@ Portal-Type: example.conference.session ``` 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. +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. -In order 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. +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. +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. +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. - -## Customising WebDAV behaviour - -There are several ways in which you can influence the WebDAV behaviour -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 serialisations and parsers for fields. - See the [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 - (e.g. an image editor if the MIME type is `image/jpeg`). - The file `plone.dexterity.filerepresentation` 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] 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 `program.py`, looks like this: +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 @@ -199,75 +170,61 @@ class ProgramFileFactory(object): return session ``` -We need to register the adapter in configure.zcml: +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. +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`. +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`. -Here is a simple automated integration test for the same component: +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'] + self.folder.invokeFactory("example.conference.program", "p1") + p1 = self.folder["p1"] fileFactory = IFileFactory(p1) - newObject = fileFactory('new-session', 'text/plain', 'dummy') + 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 customise or debug WebDAV behaviour. +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`. +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`. +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. +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. -The [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. +`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". -`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` @@ -276,260 +233,185 @@ 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`. +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 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. +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 behaviours will be turned into -UTF-8 encoded headers. -The primary field, if any, will be returned in the body, also most likely -encoded as an UTF-8 encoded string. +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 behaviour can provide its own adapter. +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. +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. +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 -behaviours, and similarly read the body and update the corresponding primary -field. - -`NullResource.PUT()` is responsible for creating a new content object -and initialising 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 (e.g. 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 initialised 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. +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. +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 initialised. +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. +`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. -`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`. +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; and -`webdav`, a `DAVProperties` instance. -The `DefaultProperties` instance contains the main property sheet. This -typically has a `title` property, for example. +- `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`, +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__`, so e.g. -reading the `creationdate` property calls `dav__creationdate()` on the +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 -(i.e. the content object). -In particular: +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). +: returns a fixed date (January 1st, 1970). `displayname` - -: returns the value of the `title_or_id()` method. +: Returns the value of the `title_or_id()` method. `resourcetype` - -: returns an empty string or ``. +: Returns an empty string or ``. `getlastmodified` - -: returns the ZODB modification time. +: 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. +: 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. +: 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. +: Returns a link to `/document_src`, if that attribute exists. `supportedlock` - -: indicates whether `IWriteLock` is supported by the content item. +: Indicates whether `IWriteLock` is supported by the content item. `lockdiscovery` +: Returns information about any active locks. -: returns information about any active locks. +Other properties in this and any other property sheets are returned as stored when requested. -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. -If the `PROPFIND` request specifies a depth of 1 or infinity -(i.e. 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. +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. -`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, -i.e. create a new folder. +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). +`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. +`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 simply calls `manage_addFolder()` on the parent. +`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. +`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. +`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. +That is, they check for locks before attempting any operation that would violate a lock. -Also note that [plone.locking] uses the lock implementation from the -`webdav` package by default. +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). +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` @@ -540,30 +422,14 @@ An `UNLOCK` request is used to unlock a locked object. `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). +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. - -[cyberduck]: http://cyberduck.ch/ -[external editor]: ../../../../../external-editor -[plone.dexterity.filerepresentation]: http://pypi.python.org/pypi/plone.dexterity.filerepresentation -[plone.locking]: http://pypi.python.org/pypi/plone.locking -[plone.recipe.zope2instance]: http://pypi.python.org/pypi/plone.recipe.zope2instance -[plone.rfc822]: http://pypi.python.org/pypi/plone.rfc822 -[plone.supermodel]: http://pypi.python.org/pypi/plone.supermodel -[zope.filerepresentation]: http://pypi.python.org/pypi/zope.filerepresentation +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. From 9185cf2db4ce5a8919a562c199c402873d2829d4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Tue, 18 Apr 2023 07:21:23 -0700 Subject: [PATCH 32/53] Tidy testing/index.md --- plone.app.dexterity/testing/index.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plone.app.dexterity/testing/index.md b/plone.app.dexterity/testing/index.md index 8dc556303..08d96a73a 100644 --- a/plone.app.dexterity/testing/index.md +++ b/plone.app.dexterity/testing/index.md @@ -1,20 +1,20 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Writing unit and integration tests** +This part describes how to write unit and integration tests for Plone content types. ```{toctree} :maxdepth: 2 -unit-tests.rst -integration-tests.rst -mock-testing.rst +unit-tests +integration-tests +mock-testing ``` From 0b3798911fedefa5028f568ab1bceaeaa8c76aa8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 19 Apr 2023 04:33:19 -0700 Subject: [PATCH 33/53] Tidy unit-tests.md --- plone.app.dexterity/testing/unit-tests.md | 141 +++++++++------------- 1 file changed, 57 insertions(+), 84 deletions(-) diff --git a/plone.app.dexterity/testing/unit-tests.md b/plone.app.dexterity/testing/unit-tests.md index 05c9c4353..df65876ab 100644 --- a/plone.app.dexterity/testing/unit-tests.md +++ b/plone.app.dexterity/testing/unit-tests.md @@ -1,55 +1,40 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Writing simple 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]. -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. +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, because: - -- 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’re writing content types, we’re -often more interested in integration test, 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 `program.py`. -We’ll add the directory `tests`, with an `__init__.py` and a file -`test_program.py` that looks like this: +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 @@ -114,58 +99,46 @@ def test_suite(): return unittest.defaultTestLoader.loadTestsFromName(__name__) ``` -This is a simple 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/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, we can do: - -```console -$ ./bin/test example.conference +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 -(like output colouring). -We could also use the built-in test runner in the `instance` script, -e.g. with `./bin/instance test -s example.conference`. -::: +```{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, we can do: +To run just this test suite, use the following command. -```console -$ ./bin/test example.conference -t TestProgramUnit +```shell +./bin/test example.conference -t TestProgramUnit ``` -This is useful when we have other test suites that we don’t want to run, -e.g. because they are integration tests and require lengthy setup. +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: +To get a report about test coverage, we can run the following command. -```console -$ ./bin/test example.conference --coverage +```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%. - -[plone testing tutorial]: /external/plone.app.testing/docs/source/index +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. From a85c02d28ef34a1297a1cc4d605350d37c1630a8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 19 Apr 2023 05:00:09 -0700 Subject: [PATCH 34/53] Tidy integration-tests.md --- .../testing/integration-tests.md | 300 ++++++++---------- 1 file changed, 125 insertions(+), 175 deletions(-) diff --git a/plone.app.dexterity/testing/integration-tests.md b/plone.app.dexterity/testing/integration-tests.md index a8d11fa1b..bd0c53b01 100644 --- a/plone.app.dexterity/testing/integration-tests.md +++ b/plone.app.dexterity/testing/integration-tests.md @@ -1,69 +1,52 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Writing integration tests with plone.app.testing** +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. +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 re-used 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. +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, erm, layered. +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. +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. +`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 *setup.py*, we will add the extras_require option, like so: +In {file}`setup.py`, we will add the `extras_require` option as shown. -``` +```python extras_require = { - 'test': ['plone.app.testing'] + "test": ["plone.app.testing"] }, ``` -:::{note} -Don’t forget to re-run buildout after making changes to `setup.py`. -::: +```{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. +`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/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 -`testing.py` file: +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 @@ -80,33 +63,29 @@ class Fixture(PloneSandboxLayer): def setUpPloneSite(self, portal): # Install the example.conference product - self.applyProfile(portal, 'example.conference:default') + self.applyProfile(portal, "example.conference:default") FIXTURE = Fixture() INTEGRATION_TESTING = IntegrationTesting( bases=(FIXTURE,), - name='example.conference:Integration', + name="example.conference:Integration", ) FUNCTIONAL_TESTING = FunctionalTesting( bases=(FIXTURE,), - name='example.conference:Functional', + 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* documentation for more details. +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.): +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 @@ -125,59 +104,59 @@ 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'] + 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.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') + fti = queryUtility(IDexterityFTI, name="example.conference.program") self.assertNotEquals(None, fti) def test_schema(self): - fti = queryUtility(IDexterityFTI, name='example.conference.program') + 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') + 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') + 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'] + 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())) + 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')) + 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'] + 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') + result = self.portal.portal_catalog(Subject="Track 2") self.assertEqual(1, len(result)) self.assertEqual(result[0].getURL(), p1.absolute_url()) @@ -186,61 +165,40 @@ 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] 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 (here, 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), - 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 (e.g. 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. +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. -```console -$ ./bin/test example.conference +```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. +Again, use the `-t` option to run a particular test case (or test method) only. -The other tests are similar. We have `tests/test_session.py` to test -the `Session` type: +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 @@ -260,78 +218,74 @@ 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'] + 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.assertRaises(ValueError, self.folder.invokeFactory, "example.conference.session", "session1") - self.folder.invokeFactory('example.conference.program', 'program1') - p1 = self.folder['program1'] + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] - p1.invokeFactory('example.conference.session', 'session1') - s1 = p1['session1'] + 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') + fti = queryUtility(IDexterityFTI, name="example.conference.session") self.assertNotEquals(None, fti) def test_schema(self): - fti = queryUtility(IDexterityFTI, name='example.conference.session') + 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') + 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'] + 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'] + 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]) + 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()) + 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'] + self.folder.invokeFactory("example.conference.program", "program1") + p1 = self.folder["program1"] - p1.invokeFactory('example.conference.session', 'session1') - s1 = p1['session1'] + p1.invokeFactory("example.conference.session", "session1") + s1 = p1["session1"] chain = self.portal.portal_workflow.getChainFor(s1) - self.assertEqual(('example.conference.session_workflow',), chain) + 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: +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 @@ -350,28 +304,28 @@ 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'] + 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.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') + fti = queryUtility(IDexterityFTI, name="example.conference.presenter") self.assertNotEquals(None, fti) def test_schema(self): - fti = queryUtility(IDexterityFTI, name='example.conference.presenter') + 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') + fti = queryUtility(IDexterityFTI, name="example.conference.presenter") factory = fti.factory new_object = createObject(factory) self.assertTrue(IPresenter.providedBy(new_object)) @@ -379,7 +333,3 @@ class TestPresenterIntegration(unittest.TestCase): def test_suite(): return unittest.defaultTestLoader.loadTestsFromName(__name__) ``` - -[collective.testcaselayer]: http://pypi.python.org/pypi/collective.testcaselayer -[plone.app.testing]: http://pypi.python.org/pypi/plone.app.testing -[testing tutorial]: /external/plone.app.testing/docs/source/index From 8c9e9a41c95ba2b0460133dbd6bf25975ff45559 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 19 Apr 2023 16:23:07 -0700 Subject: [PATCH 35/53] Tidy mock-testing.md --- plone.app.dexterity/testing/mock-testing.md | 277 ++++++++------------ 1 file changed, 109 insertions(+), 168 deletions(-) diff --git a/plone.app.dexterity/testing/mock-testing.md b/plone.app.dexterity/testing/mock-testing.md index 85ca0532c..533e25147 100644 --- a/plone.app.dexterity/testing/mock-testing.md +++ b/plone.app.dexterity/testing/mock-testing.md @@ -1,32 +1,29 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Using a mock objects framework to write mock based tests** +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. +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 like this: +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') + 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') + sender = portal.getProperty("email_from_address") if not sender: return @@ -36,78 +33,59 @@ def notifyUser(presenter, event): matching_users = acl_users.searchUsers(fullname=presenter.title) for user_info in matching_users: - email = user_info.get('email', None) + 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, e.g. 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, or the number of times a method is - called or an attribute is retrieved or set. -- You can also give your mock objects behaviour, e.g. by specifying - return values or exceptions to be raised in certain cases. -- Initialise the code under test and/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 (e.g. with the wrong arguments, or too many times) - or insufficiently (e.g. an expected method was not called). - -There are several Python mock object frameworks. Dexterity itself users -a powerful one called [mocker], via the [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 *setup.py* and re-run -buildout. +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', + # ... + "plone.mocktestcase", ], ``` -As an example test case, consider the following class in -*test_presenter.py*: +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 @@ -122,14 +100,14 @@ class TestPresenterUnit(MockTestCase): __parent__=None, __name__=None, title="Jim", - absolute_url = lambda: 'http://example.org/presenter', + 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'}] + user_info = [{"email": "jim@example.org", "id": "jim"}] # email data message = "A presenter called Jim was added here http://example.org/presenter" @@ -140,18 +118,18 @@ class TestPresenterUnit(MockTestCase): # mock tools/portal portal_mock = self.mocker.mock() - self.expect(portal_mock.getProperty('email_from_address')).result('test@example.org') + 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.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) + 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.mock_tool(mail_host_mock, "MailHost") self.expect(mail_host_mock.send(message, email, sender, subject)) @@ -165,99 +143,62 @@ class TestPresenterUnit(MockTestCase): # 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, - e.g. 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, e.g.: - -``` -$ ./bin/test example.conference -t TestPresenterMock +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. +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, for example by [Martin Fowler]. - -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. - -[martin fowler]: http://www.martinfowler.com/articles/mocksArentStubs.html -[mocker]: http://labix.org/mocker -[plone.mocktestcase]: http://pypi.python.org/pypi/plone.mocktestcase +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. From 082d478f35f6c476236743fcc3997b51e147b94c Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 19 Apr 2023 16:24:37 -0700 Subject: [PATCH 36/53] Tidy reference/index.md and correct to American English spelling --- plone.app.dexterity/reference/index.md | 14 +++++++------- ...tandard-behaviours.md => standard-behaviors.md} | 0 2 files changed, 7 insertions(+), 7 deletions(-) rename plone.app.dexterity/reference/{standard-behaviours.md => standard-behaviors.md} (100%) diff --git a/plone.app.dexterity/reference/index.md b/plone.app.dexterity/reference/index.md index 8181b7f0b..4efc44a0b 100644 --- a/plone.app.dexterity/reference/index.md +++ b/plone.app.dexterity/reference/index.md @@ -14,11 +14,11 @@ myst: ```{toctree} :maxdepth: 2 -fields.rst -widgets.rst -standard-behaviours.rst -form-schema-hints.rst -manipulating-content-objects.rst -dexterity-xml.rst -misc.rst +fields +widgets +standard-behaviors +form-schema-hints +manipulating-content-objects +dexterity-xml +misc ``` diff --git a/plone.app.dexterity/reference/standard-behaviours.md b/plone.app.dexterity/reference/standard-behaviors.md similarity index 100% rename from plone.app.dexterity/reference/standard-behaviours.md rename to plone.app.dexterity/reference/standard-behaviors.md From 2e9ff77d4c7f3b908e9f546c4a48462ff39a3fb1 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 19 Apr 2023 16:26:01 -0700 Subject: [PATCH 37/53] Tidy reference/index.md --- plone.app.dexterity/reference/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plone.app.dexterity/reference/index.md b/plone.app.dexterity/reference/index.md index 4efc44a0b..5fe67cf7a 100644 --- a/plone.app.dexterity/reference/index.md +++ b/plone.app.dexterity/reference/index.md @@ -1,15 +1,15 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Useful references for things like field types, wigets and APIs** +This part of the documentation describes useful references for field types, widgets, and APIs. ```{toctree} :maxdepth: 2 From 01977660abe8d4fb02c4c492b60fa55675b86d76 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 20 Apr 2023 06:18:27 -0700 Subject: [PATCH 38/53] tidy reference/fields.md, add label --- plone.app.dexterity/reference/fields.md | 191 ++++++++++----------- plone.app.dexterity/schema-driven-types.md | 2 + 2 files changed, 93 insertions(+), 100 deletions(-) diff --git a/plone.app.dexterity/reference/fields.md b/plone.app.dexterity/reference/fields.md index 8989f6639..9ae55c8bf 100644 --- a/plone.app.dexterity/reference/fields.md +++ b/plone.app.dexterity/reference/fields.md @@ -1,131 +1,122 @@ --- myst: - html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + 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 -**The standard schema 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. -The following tables shows the most common field types for use in -Dexterity schemata. -See the documentation on [creating schemata] for information about how to -use these. ## Field properties -Fields are initialised 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 | unicode | 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/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, e.g. for an `Int` field this should be an integer. Default is `None` (no check). | -| | max | | The maximum allowed value. Must be a valid value for the field, e.g. for an Int field this 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 `Markup` control panel) will be used. | +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 [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, i.e. a `Bytes` with newlines 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 (e.g. 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]. Not available by default in Zope 2.10. | IField, IMinMax | +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] and [plone.formwidget.namedfile] for more -details. +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 | -| 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] for more details. +See [`z3c.relationfield`](https://pypi.org/project/z3c.relationfield/) for more details. -| Name | Type | Description | Properties | +| 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` | +| 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] +### Fields in `plone.app.textfield` -See [plone.app.textfield] for more details. +See [`plone.app.textfield`](https://pypi.org/project/plone.app.textfield/) for more details. -| Name | Type | Description | Properties | +| 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] for more details. +### Fields in `plone.schema` + +See [`plone.schema`](https://pypi.org/project/plone.schema/) for more details. -| Name | Type | Description | Properties | +| Name | Type | Description | Properties | | ----- | ---- | ----------------------------------- | ------------------ | -| Email | str | A field containing an email address | IField, IMinMaxLen | - -[creating schemata]: ../schema-driven-types.html#the-schema -[plone.app.textfield]: http://pypi.python.org/pypi/plone.app.textfield -[plone.formwidget.namedfile]: http://pypi.python.org/pypi/plone.formwidget.namedfile -[plone.namedfile]: http://pypi.python.org/pypi/plone.namedfile -[plone.schema]: http://pypi.python.org/pypi/plone.schema -[vocabularies]: ../advanced/vocabularies.html -[z3c.relationfield]: http://pypi.python.org/pypi/z3c.relationfield -[zope.schema]: http://pypi.python.org/pypi/zope.schema +| Email | str | A field containing an email address | IField, IMinMaxLen | diff --git a/plone.app.dexterity/schema-driven-types.md b/plone.app.dexterity/schema-driven-types.md index 0b72b90a9..dcb5ca76b 100644 --- a/plone.app.dexterity/schema-driven-types.md +++ b/plone.app.dexterity/schema-driven-types.md @@ -12,6 +12,8 @@ myst: 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. From f4085157e6637ca28d1300f0760da1f5f8d3e1a2 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 20 Apr 2023 06:24:54 -0700 Subject: [PATCH 39/53] Tidy widgets.md --- plone.app.dexterity/reference/widgets.md | 45 +++++++++++------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/plone.app.dexterity/reference/widgets.md b/plone.app.dexterity/reference/widgets.md index ad0e3f803..5e83ca50a 100644 --- a/plone.app.dexterity/reference/widgets.md +++ b/plone.app.dexterity/reference/widgets.md @@ -1,37 +1,32 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Standard and common third party 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][z3c.form documentation]. To learn about setting custom widgets for Dexterity -content types, see the [schema introduction]. +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 | +| 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 [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/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. | - -[schema introduction]: ../schema-driven-types.html#the-schema -[vocabularies]: ../advanced/vocabularies.html -[z3c.form documentation]: https://z3cform.readthedocs.io/en/latest/widgets/index.html +| 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. | From 642a5bc8dd9079369e96030495472fc93f035697 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 20 Apr 2023 06:25:30 -0700 Subject: [PATCH 40/53] Fix indentation --- plone.app.dexterity/reference/fields.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plone.app.dexterity/reference/fields.md b/plone.app.dexterity/reference/fields.md index 9ae55c8bf..762791d28 100644 --- a/plone.app.dexterity/reference/fields.md +++ b/plone.app.dexterity/reference/fields.md @@ -1,10 +1,10 @@ --- 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" + 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 From 62966676ca7a73a0148159808fd891cd406a90f5 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 20 Apr 2023 06:30:18 -0700 Subject: [PATCH 41/53] Tidy standard-behaviors.md --- .../reference/standard-behaviors.md | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/plone.app.dexterity/reference/standard-behaviors.md b/plone.app.dexterity/reference/standard-behaviors.md index e81228e30..c9362c077 100644 --- a/plone.app.dexterity/reference/standard-behaviors.md +++ b/plone.app.dexterity/reference/standard-behaviors.md @@ -1,38 +1,38 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**A list of common behaviors that ship with Plone and Dexterity** +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. +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) and/or icons and to override thumb size in listings or tables. | +| 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. | From 940939dc030554a21e477270b15ac81bd4442390 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 20 Apr 2023 06:43:03 -0700 Subject: [PATCH 42/53] Tidy form-schema-hints.md --- .../reference/form-schema-hints.md | 111 +++++++++--------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/plone.app.dexterity/reference/form-schema-hints.md b/plone.app.dexterity/reference/form-schema-hints.md index 66e5d6b61..ffa27cdb0 100644 --- a/plone.app.dexterity/reference/form-schema-hints.md +++ b/plone.app.dexterity/reference/form-schema-hints.md @@ -1,24 +1,24 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 +# Form configuration with schema hints using directives -Dexterity uses the directives in [plone.autoform](http://pypi.python.org/pypi/plone.autoform) and [plone.supermodel](http://pypi.python.org/pypi/plone.supermodel) package to configure its [z3c.form](http://docs.zope.org/z3c.form)-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. +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](http://pypi.python.org/pypi/plone.autoform) and [plone.supermodel](http://pypi.python.org/pypi/plone.supermodel). -For the directives to work, the schema must derive from *plone.supermodel.model.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 they apply to. +By convention they are kept next to the fields to which they apply. -For example, here is a schema that omits a field: +For example, here is a schema that omits a field. ```python from plone.autoform import directives @@ -28,41 +28,41 @@ from zope import schema class ISampleSchema(model.Schema): - title = schema.TextLine(title='Title') + title = schema.TextLine(title="Title") - directives.omitted('additionalInfo') + directives.omitted("additionalInfo") additionalInfo = schema.Bytes() ``` -The form directives are taking parameters in the form of a list of field names, -or a set of field name/value pairs as keyword arguments. +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: +There are two kinds of directives. -- appearance related directives -- security related directives +- Appearance related directives +- Security related directives -## Appearance Related Directives -*plone.autoform.directives* provides these: +## 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. | +| - | -| +| 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 (i.e. one from a behavior), its name will be e.g. `IOtherSchema.other_field_name`. If the other field is from the same schema, its name can be abbreviated by a leading dot e.g. `.other_field_name`. If the other field is is used without a prefix, its is looked up from the main schema e.g. `other_field_name`. Alternatively, pass the string “\*” to put a field first in the fieldsets form. | -| 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. | +| 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 these: +`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: +The code sample below illustrates each of these directives. ```python from plone.autoform import directives @@ -75,70 +75,71 @@ 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 + # 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'] + 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') + directives.widget(body="plone.app.z3cform.wysiwyg.WysiwygFieldWidget") + primary("body") body = schema.Text( - title='Body text', + title="Body text", required=False, - default='Body text goes here' + default="Body text goes here" ) # The widget can also be specified as an object directives.widget(footer=WysiwygFieldWidget) footer = schema.Text( - title='Footer text', + title="Footer text", required=False ) # An omitted field. - # Use directives.omitted('a', 'b', 'c') to omit several fields + # Use directives.omitted("a", "b", "c") to omit several fields - directives.omitted('dummy') + directives.omitted("dummy") dummy = schema.Text( - title='Dummy' + title="Dummy" ) - # A field in 'hidden' mode + # A field in "hidden" mode - directives.mode(secret='hidden') + directives.mode(secret="hidden") secret = schema.TextLine( - title='Secret', - default='Secret stuff' + title="Secret", + default="Secret stuff" ) - # This field is moved before the 'description' field of the standard + # This field is moved before the "description" field of the standard # IDublinCore behavior, if this is in use. - directives.order_before(importantNote='IDublinCore.description') + directives.order_before(importantNote="IDublinCore.description") importantNote = schema.TextLine( - title='Important note', + title="Important note", ) ``` + ## Security related directives -The security directives in the *plone.autoform.directives* module are shown below. +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. | +| 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: +The code sample below illustrates each of these directives. ```python from plone.autoform import directives @@ -147,13 +148,13 @@ from zope import schema class ISampleSchema(model.Schema): - # This field requires the 'cmf.ReviewPortalContent' permission + # This field requires the "cmf.ReviewPortalContent" permission # to be read and written - directives.read_permission(reviewNotes='cmf.ReviewPortalContent') - directives.write_permission(reviewNotes='cmf.ReviewPortalContent') + directives.read_permission(reviewNotes="cmf.ReviewPortalContent") + directives.write_permission(reviewNotes="cmf.ReviewPortalContent") reviewNotes = schema.Text( - title='Review notes', + title="Review notes", required=False, ) ``` From f6ec9842443480cd9252517712dfd81980d75410 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Thu, 20 Apr 2023 07:35:39 -0700 Subject: [PATCH 43/53] Tidy manipulating-content-objects.md --- .../reference/manipulating-content-objects.md | 331 ++++++++---------- 1 file changed, 155 insertions(+), 176 deletions(-) diff --git a/plone.app.dexterity/reference/manipulating-content-objects.md b/plone.app.dexterity/reference/manipulating-content-objects.md index c6aacaeda..f211eca4f 100644 --- a/plone.app.dexterity/reference/manipulating-content-objects.md +++ b/plone.app.dexterity/reference/manipulating-content-objects.md @@ -1,31 +1,30 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -:::{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. -::: +This chapter describes common APIs used to manipulate Dexterity content objects. -**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. -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 means to create objects and manipulate folders. +This section describes the means to create objects and manipulate folders. + ### Creating a content object @@ -33,350 +32,330 @@ The simplest way to create a content item is via its factory: ```python from zope.component import createObject -context = createObject('example.type') +context = createObject("example.type") ``` -At this point, the object is not acquisition wrapped. You can wrap it -explicitly by calling: +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. +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. +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: +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") +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. +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 (e.g. a *Large Plone Folder* in Plone 3 or a container based on -*plone.folder*), you can do: +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 +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. +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: +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 +folder._setObject("some_id") = context ``` -Note that both of these approaches bypass any type checks, i.e. 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: +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, so things like 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). +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*. +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: +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") +createContentInContainer(folder, "example.type", title="Foo") ``` -Again, you can pass *checkConstraints=False* to bypass folder -constraints, and pass object properties as keyword arguments. +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: +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'] +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. +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: +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'] +context = folder["some_id"] ``` -Folders can also be iterated over, and you can all *items()*, *keys()*, -*values()* and so on, treating the folder as a dict with string keys and -content objects as values. +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. -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 so implement -*\_\_delitem\_\_*: +Again, Dexterity containers act like dictionaries, and thus implement `__delitem__`. ```python -del folder['some_id'] +del folder["some_id"] ``` -The OFS API uses the *\_delObject()* function for the same purpose: +The OFS API uses the `_delObject()` function for the same purpose. ```python -folder._delObject('some_id') +folder._delObject("some_id") ``` + ## Object introspection -This section describes means of getting information about an object. +This section describes the means of getting information about an object. + -### Obtaining an object’s schema interface +### Obtaining an object's schema interface -A content object’s schema is an interface, i.e. an object of type -*zope.interface.interface.InterfaceClass*. +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. For example: +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: +### 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) - ... + 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. +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: +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') +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 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 containment parent: +### 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. +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: +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. +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: +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') +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. +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: +To invoke a transition, use the following. ```python -portal_workflow.doActionFor(context, 'some_transition') +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. +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. +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 actually to send an event and let Dexterity’s -standard event handlers take care of this: +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: +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: +You can also pass specific index names to reindex, if you don't want to reindex everything. ```python -context.reindexObject(idxs=['Title', 'sortable_title']) +context.reindexObject(idxs=["Title", "sortable_title"]) ``` -This method comes from the -*Products.CMFCore.CMFCatalogAware.CMFCatalogAware* mix-in class. +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 [permissions]. +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: +To check a permission by its Zope 3 name, use the following. ```python from zope.security import checkPermission -checkPermission('zope2.View', context) +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 (e.g. the *afterSetUp()* method). +```{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). +``` -To use the Zope 2 permission title: +Use the Zope 2 permission title. ```python from AccessControl import getSecurityManager -getSecurityManager().checkPermission('View', context) +getSecurityManager().checkPermission("View", context) ``` -Sometimes, normally in tests, you want to know which roles have a -particular permission. To do this, use: +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']] +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: +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) +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. +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 | +| 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 1st, 1970. | -| modified() | DateTime | Returns the Zope 2 DateTime for the object’s modification date. If not set, returns a “floor” date of January 1st, 1970. | -| effective() | DateTime | Returns the Zope 2 DateTime for the object’s effective date. If not set, returns a “floor” date of January 1st, 1970. | -| expires() | DateTime | Returns the Zope 2 DateTime for the object’s expiration date. If not set, returns a “floor” date of January 1st, 1970. | - -[permissions]: ../advanced/permissions.html +| __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. | From f3582cf4575096e800a93fbbb4179aaa987d4774 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 03:11:35 -0700 Subject: [PATCH 44/53] Tidy dexterity-xml.md --- .../reference/dexterity-xml.md | 151 ++++++++---------- 1 file changed, 71 insertions(+), 80 deletions(-) diff --git a/plone.app.dexterity/reference/dexterity-xml.md b/plone.app.dexterity/reference/dexterity-xml.md index af23a1aee..fed6ead88 100644 --- a/plone.app.dexterity/reference/dexterity-xml.md +++ b/plone.app.dexterity/reference/dexterity-xml.md @@ -1,28 +1,29 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**A reference for Dexterity's XML name spaces** +This chapter serves as a reference for Dexterity's XML name spaces. + ## Introduction -The schema (structure) of a Dexterity content type may be detailed in two very different ways: +The schema of a Dexterity content type may be detailed in two very different ways. -> - In Python as a Zope schema; or, -> - In XML +- In Python as a Zope schema +- In XML -When you are using Dexterity's through-the-web 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. +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 like: +That's typically done with code such as the following. ```python class IExampleType(form.Schema): @@ -32,7 +33,7 @@ class IExampleType(form.Schema): or: -``` +```python from plone.supermodel import xmlSchema IExampleType = xmlSchema("models/example_type.xml") @@ -43,10 +44,11 @@ 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 + +## 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 typical structure of a Dexterity XML document is the following. ```xml @@ -63,12 +65,13 @@ The typical structure of a Dexterity XML document is: ``` -Only the default name space (.../supermodel/schema) is required for basic schema. +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: +## `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 @@ -83,11 +86,12 @@ Most of the supermodel/schema field tag and its attributes map directly to what' ``` -The field `type` needs to be the full dotted name (as if it was being imported in Python) of the field type. +The field `type` needs to be the full dotted name of the field type, as if it were imported in Python. + ### Fieldsets -It's easy to add fieldsets by surrounding embedding fields tags in a `fieldset` block: +To add fieldsets, surround embedded `field` tags in a `fieldset` block. ```xml @@ -108,9 +112,10 @@ It's easy to add fieldsets by surrounding embedding fields tags in a `fieldset` ``` + ### Vocabularies -Vocabularies may be specified via dotted names using the `source` tag: +Vocabularies may be specified via dotted names using the `source` tag. ```xml @@ -124,15 +129,15 @@ Vocabularies may be specified via dotted names using the `source` tag: ``` -Where the full Python dotted-name of a Zope vocabulary in a package: +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')]) +dummy_vocabulary_instance = SimpleVocabulary.fromItems([(1, "a"), (2, "c")]) ``` -Or, a source binder: +Or, a source binder. ```xml @@ -141,7 +146,7 @@ Or, a source binder: ``` -With Python like: +Or in Python. ```python from zope.schema.interfaces import IContextSourceBinder @@ -150,20 +155,20 @@ class Binder(object): implements(IContextSourceBinder) def __call__(self, context): - return SimpleVocabulary.fromValues(['a', 'd', 'f']) + 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. +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: +Note that we need to add the `i18n` namespace and a domain specification. ```xml ``` -## supermodel/form attributes -supermodel/form provides attributes that govern presentation and editing. +## `supermodel/form` attributes -### after/before +`supermodel/form` provides attributes that govern presentation and editing. -To re-order 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 field in the -current schema (or a base schema). Use a fully prefixed name (e.g. -`'my.package.ISomeSchema'`) to refer to a field in another schema. Use an -unprefixed name to refer to a field in the default schema for the form. +### `after/before` -Example: +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 ``` -### 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`. +### `mode` -Example: +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 ``` -### omitted -To omit a field from all forms, use `form:omitted="true"`. To omit a field -only from some forms, specify a form interface like -`form:omitted="z3c.form.interfaces.IForm:true"`. Multiple interface:value -settings may be specified, separated by spaces. +### `omitted` -Examples: +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 ``` -### 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. +### Dynamic defaults -Example: +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 @@ -275,15 +270,15 @@ Example: ``` -Sample Python for the validator factory: +Sample Python for the validator factory. ```python @provider(IDefaultFactory) def dummy_defaultFactory(): - return 'something' + return "something" ``` -For a callable using context: +For a callable using context. ```python @provider(IContextAwareDefaultFactory) @@ -291,18 +286,15 @@ def dummy_defaultCAFactory(context): return context.something ``` -:::{note} -The `defaultFactory` tag was added in plone.supermodel 1.2.3, -shipping with Plone 4.3.2+. -::: +```{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. +### `validator` -Example: +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 ``` -Sample Python for the validator factory: +Sample Python for the validator factory. ```python class TestValidator(z3c.form.validator.SimpleFieldValidator): @@ -322,17 +314,16 @@ class TestValidator(z3c.form.validator.SimpleFieldValidator): raise Invalid("Test") ``` + (dexterity-xml-security)= -## supermodel/security attributes +## `supermodel/security` attributes -### read-permission/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. +### `read-permission` and `write-permission` -Example: +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 Date: Fri, 21 Apr 2023 03:17:04 -0700 Subject: [PATCH 45/53] Tidy misc.md --- plone.app.dexterity/reference/misc.md | 28 ++++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/plone.app.dexterity/reference/misc.md b/plone.app.dexterity/reference/misc.md index 39482a445..babcfc57e 100644 --- a/plone.app.dexterity/reference/misc.md +++ b/plone.app.dexterity/reference/misc.md @@ -1,30 +1,26 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 +# Miscellaneous recipes -## User contributed recipes +## Hiding a field -**How to hide a field on a schema if we do not want to or cannot modify the original schema** - -To do this one can use tagged values on the schema. In this case want to hide 'introduction' and 'answers' fields: +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')]) + [(Interface, "introduction", "true"), + (Interface, "answers", "true")]) ``` -This code can sit in another.package.\_\_init\_\_.py for example. - -See also: [Original thread on coredev mailinglist] - -[original thread on coredev mailinglist]: http://plone.293351.n2.nabble.com/plone-autoform-why-use-tagged-values-td7560956.html +This code can reside in {file}`another.package.__init__.py`. From 9ee3e86b1a54af4003ec81534e516445f1f7c432 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 03:18:03 -0700 Subject: [PATCH 46/53] Grammar --- plone.app.dexterity/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plone.app.dexterity/install.md b/plone.app.dexterity/install.md index ae024b086..f9264f9e3 100644 --- a/plone.app.dexterity/install.md +++ b/plone.app.dexterity/install.md @@ -16,6 +16,6 @@ Dexterity is already installed as part of Plone 5.x and later. No further action is needed for these versions. ``` -## Install Dexterity on Plone 4.3 +## Install Dexterity in Plone 4.3 See [Installing Dexterity](https://4.docs.plone.org/external/plone.app.dexterity/docs/install.html). From aad39ee5dd2f3c3df9913ad4de3afcce1fcf8add Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 03:28:11 -0700 Subject: [PATCH 47/53] Tidy index.md and intro.md --- plone.app.dexterity/behaviors/index.md | 10 +++---- plone.app.dexterity/behaviors/intro.md | 41 +++++++++++--------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/plone.app.dexterity/behaviors/index.md b/plone.app.dexterity/behaviors/index.md index cdb466434..52bb71f23 100644 --- a/plone.app.dexterity/behaviors/index.md +++ b/plone.app.dexterity/behaviors/index.md @@ -1,15 +1,15 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**How to create re-usable behaviors for Dexterity types** +This section describes how to create reusable behaviors for Dexterity content types. ```{toctree} :maxdepth: 2 diff --git a/plone.app.dexterity/behaviors/intro.md b/plone.app.dexterity/behaviors/intro.md index ffb6ecbc6..c236c1670 100644 --- a/plone.app.dexterity/behaviors/intro.md +++ b/plone.app.dexterity/behaviors/intro.md @@ -1,44 +1,39 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**About this manual** - 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 re-usable bundles of functionality that can be enabled or disabled on a per-content type basis. +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 and/or marker interface. +- 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 (e.g. adapters, event handlers, views, viwelets) once and re-use them easily. -- A more experienced developer is making functionality available for re-use by less experienced integrators. - For example, a behavior can be packaged up and release 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. +- 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. +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 [Dexterity Developer Manual] for more information about how to enable behaviors on a type and for a list of standard behaviors. - -To learn more about how behaviors in detail are implemented, see the [plone.behavior] package. +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. -[dexterity developer manual]: ../index.html -[plone.behavior]: http://pypi.python.org/pypi/plone.behavior +To learn more about how behaviors are implemented in detail, see the [`plone.behavior`](https://pypi.org/project/plone.behavior/) package. From dd6771fe6f800f812cb2123f8e3c332214a1982f Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 03:32:22 -0700 Subject: [PATCH 48/53] Tidy behavior-basics.md --- .../behaviors/behavior-basics.md | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/plone.app.dexterity/behaviors/behavior-basics.md b/plone.app.dexterity/behaviors/behavior-basics.md index 4cf8ca0fc..24f4310fb 100644 --- a/plone.app.dexterity/behaviors/behavior-basics.md +++ b/plone.app.dexterity/behaviors/behavior-basics.md @@ -1,34 +1,35 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**The fundamental concepts behind behaviors** +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?" +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. +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 consist at the very least of an interface and some metadata, namely a title and a description. +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. +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. +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. +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. From 27c63e98b3af6362cbd00217ab073b20d564e8b6 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 03:39:34 -0700 Subject: [PATCH 49/53] Tidy creating-and-registering-behaviors.md --- .../creating-and-registering-behaviors.md | 81 +++++++++---------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md b/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md index e63010fd5..74e61704a 100644 --- a/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md +++ b/plone.app.dexterity/behaviors/creating-and-registering-behaviors.md @@ -1,36 +1,35 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**How to create a basic behavior that provides form fields** +This chapter describes how to create a basic behavior that provides form fields. -The following example is based on the [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. +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. +`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 *setup.py*: +First, there are a few dependencies in {file}`setup.py`. ```python install_requires=[ - ..., - 'plone.behavior', - 'zope.schema', - 'zope.interface', - 'zope.component', + # ..., + "plone.behavior", + "zope.schema", + "zope.interface", + "zope.component", ], ``` -Next, we have *behaviors.zcml*, which is included from *configure.zcml* and contains all necessary configuration to set up the behaviors. -It looks like this: +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. +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. +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. +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 *behaviors.py* module looks like this: +The following is the {file}`behaviors.py` module. ```python -"""Behaviours to assign tags (to ideas). +"""Behaviors to assign tags (to ideas). -Includes a form field and a behaviour adapter that stores the data in the +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 like this: +# 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 @@ -86,9 +85,9 @@ class ITags(model.Schema): """ directives.fieldset( - 'categorization', - label=_('Categorization'), - fields=('tags',), + "categorization", + label=_("Categorization"), + fields=("tags",), ) tags = Tags( @@ -120,24 +119,20 @@ class Tags(object): 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. +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 [Dexterity Developer Manual] for more information about setting form hints in schema interfaces. +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. +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*). +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. +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. - -[collective.gtags]: http://svn.plone.org/svn/collective/collective.gtags -[dexterity developer manual]: ../index.html +It implements the interface, here by storing values in the `Subject` field. From de8f3abf5b0daae2bfe9c88f9a51c73ed217cfc4 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 03:49:09 -0700 Subject: [PATCH 50/53] Tidy providing-marker-interfaces.md --- .../behaviors/providing-marker-interfaces.md | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/plone.app.dexterity/behaviors/providing-marker-interfaces.md b/plone.app.dexterity/behaviors/providing-marker-interfaces.md index 204691836..768f10f9f 100644 --- a/plone.app.dexterity/behaviors/providing-marker-interfaces.md +++ b/plone.app.dexterity/behaviors/providing-marker-interfaces.md @@ -1,39 +1,39 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**How to use behaviors to set marker interfaces on instances of a given type.** +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. +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. +The same principle can be applied to event handlers, views, and other components. -:::{note} +```{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. -E.g. to use a particular extension point provided by another component. -In this case, it may easier to set a marker interface and provide an adapter from this marker. -::: +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. -*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. -- 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*. -For example: +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] registered for the *IWantAPony* marker interface. -If the behavior is enabled for a particular object, *IWantAPony.providedBy(object)* would be true. +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 (e.g. 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*. +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. +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 +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. +- an `ILocalRoleProvider` adapter to automatically grant local roles to the chosen reviewers +- a custom indexer that lists the reviewers -The ZCML registration looks like this: +The ZCML registration would be the following. ```xml ``` -Notice the use of the *AnnotationStorage* factory. -This is a re-usable factory that can be used to easily create behaviors from schema interfaces that store their values in annotations. -We’ll describe this in more detail later. -We could just as easily have provided our own factory in this example. +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 *reviewers.py* module contains the following: +The {file}`reviewers.py` module contains the following. ```python """Behavior to enable certain users to nominate reviewers @@ -106,21 +106,21 @@ class IReviewers(model.Schema): """ directives.fieldset( - 'ownership', - label=_('Ownership'), + "ownership", + label=_("Ownership"), fields=( - 'official_reviewers', - 'unofficial_reviewers' + "official_reviewers", + "unofficial_reviewers" ), ) directives.widget(official_reviewers=AutocompleteMultiFieldWidget) - directives.write_permission(official_reviewers='iz.EditOfficialReviewers') + directives.write_permission(official_reviewers="iz.EditOfficialReviewers") official_reviewers = schema.Tuple( - title=_('Official reviewers'), + title=_("Official reviewers"), description=_( - 'People or groups who may review this item in an official ' - 'capacity.' + "People or groups who may review this item in an official " + "capacity." ), value_type=schema.Choice( title=_("Principal"), @@ -131,12 +131,12 @@ class IReviewers(model.Schema): ) directives.widget(unofficial_reviewers=AutocompleteMultiFieldWidget) - directives.write_permission(unofficial_reviewers='iz.EditUnofficialReviewers') + directives.write_permission(unofficial_reviewers="iz.EditUnofficialReviewers") unofficial_reviewers = schema.Tuple( - title=_('Unofficial reviewers'), + title=_("Unofficial reviewers"), description=_( - 'People or groups who may review this item in a supplementary ' - 'capacity' + "People or groups who may review this item in a supplementary " + "capacity" ), value_type=schema.Choice( title=_("Principal"), @@ -173,9 +173,9 @@ class ReviewerLocalRoles(object): return () if principal_id in c.official_reviewers: - return ('Reviewer', 'OfficialReviewer',) + return ("Reviewer", "OfficialReviewer",) elif principal_id in c.unofficial_reviewers: - return ('Reviewer',) + return ("Reviewer",) return () @@ -192,17 +192,17 @@ class ReviewerLocalRoles(object): for principal_id in c.official_reviewers: seen.add(principal_id) - yield (principal_id, ('Reviewer', 'OfficialReviewer'),) + yield (principal_id, ("Reviewer", "OfficialReviewer"),) for principal_id in c.unofficial_reviewers: if principal_id not in seen: - yield (principal_id, ('Reviewer',),) + yield (principal_id, ("Reviewer",),) @implementer(IIndexer) @adapter(IReviewersMarker, IZCatalog) class ReviewersIndexer(object): - """Catalog indexer for the 'reviewers' index. + """Catalog indexer for the "reviewers" index. """ def __init__(self, context, catalog): @@ -214,24 +214,24 @@ class ReviewersIndexer(object): return tuple(set(official + unofficial)) ``` -Note that the *iz.EditOfficialReviewers* and *iz.EditUnofficialReviewers* permissions are defined and granted elsewhere. +Note that the `iz.EditOfficialReviewers` and `iz.EditUnofficialReviewers` permissions are defined and granted elsewhere. -We need to register these components in *configure.zcml*: +We need to register these components in {file}`configure.zcml`. ```xml ``` -This is quite a 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*. +This is a quite complex behavior, but hopefully you can see what's going on: -Although this behavior provides a lot of functionality, it is no more difficult for integrators to use than any other: -they would simply 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 re-usable behaviors, which can then be enabled on a per-type basis by integrators (or the same developers in lazier moments). +- 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`. -[plone.pony]: http://pypi.python.org/pypi/plone.pony +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. From 3694766bee74949f2fa3f577a6303974af8b3605 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 04:15:31 -0700 Subject: [PATCH 51/53] Tidy schema-only-behaviors.md --- .../behaviors/schema-only-behaviors.md | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/plone.app.dexterity/behaviors/schema-only-behaviors.md b/plone.app.dexterity/behaviors/schema-only-behaviors.md index 1078c04a7..03c12647e 100644 --- a/plone.app.dexterity/behaviors/schema-only-behaviors.md +++ b/plone.app.dexterity/behaviors/schema-only-behaviors.md @@ -1,30 +1,31 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**Writing behaviors that provide schema fields** +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 to writing any other schema interface. +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. +By default, `plone.behavior` provides two alternatives. + ## Using annotations -Annotations, as provided by the [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 means you can simply 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. +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: +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 simply 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 normal. +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: -simply store the attributes of the schema interface directly on the content object. +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*: +As an example, here's the standard `IRelatedItems` behavior from `plone.app.dexerity`. ```xml ``` -The *IRelatedItems* schema looks like this: +The following is the `IRelatedItems` schema. ```python from plone.autoform.interfaces import IFormFieldProvider @@ -74,8 +74,8 @@ class IRelatedItems(model.Schema): """Behavior interface to make a type support related items. """ - form.fieldset('categorization', label="Categorization", - fields=['relatedItems']) + form.fieldset("categorization", label="Categorization", + fields=["relatedItems"]) relatedItems = RelationList( title="Related Items", @@ -86,31 +86,29 @@ class IRelatedItems(model.Schema): ) ``` -This is a standard schema using *plone.autoform.directives*. +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 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. - Background: 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. +- 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 BLOBs 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. +- 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 -::: - -[zope.annotation]: http://pypi.python.org/pypi/zope.annotation +```{note} +"The moral of this story? BTrees do not always make things more efficient!" ~ Laurence Rowe +``` From 6ae26f67d88aa6bc9cb1393d2a75ed3fd8bb3173 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 04:15:58 -0700 Subject: [PATCH 52/53] Strip file suffix --- plone.app.dexterity/behaviors/index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plone.app.dexterity/behaviors/index.md b/plone.app.dexterity/behaviors/index.md index 52bb71f23..e5224e38c 100644 --- a/plone.app.dexterity/behaviors/index.md +++ b/plone.app.dexterity/behaviors/index.md @@ -14,10 +14,10 @@ This section describes how to create reusable behaviors for Dexterity content ty ```{toctree} :maxdepth: 2 -intro.rst -behavior-basics.rst -creating-and-registering-behaviors.rst -providing-marker-interfaces.rst -schema-only-behaviors.rst -testing-behaviors.rst +intro +behavior-basics +creating-and-registering-behaviors +providing-marker-interfaces +schema-only-behaviors +testing-behaviors ``` From a5e04d5273de919767f09ca6b38ba2de265f5864 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 21 Apr 2023 04:34:35 -0700 Subject: [PATCH 53/53] Tidy testing-behaviors.md --- .../behaviors/testing-behaviors.md | 279 +++++++++--------- 1 file changed, 144 insertions(+), 135 deletions(-) diff --git a/plone.app.dexterity/behaviors/testing-behaviors.md b/plone.app.dexterity/behaviors/testing-behaviors.md index ec545e4e6..5a08d5b00 100644 --- a/plone.app.dexterity/behaviors/testing-behaviors.md +++ b/plone.app.dexterity/behaviors/testing-behaviors.md @@ -1,105 +1,109 @@ --- myst: html_meta: - "description": "" - "property=og:description": "" - "property=og:title": "" - "keywords": "" + "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 -**How to write unit tests for 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 are writing a behavior with just a marker interface or schema interface, it is probably not necessary to test the interface. +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. +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. +The following example is taken from `collective.gtags` and tests the `ITags` behavior we saw on the first page of this manual. -``` -Behaviors -========= + +## 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. - -To learn more about the `Tags` field and how it works, see `tagging.rst`. - -Test setup ----------- - -Before we can run these tests, we need to load the collective.gtags -configuration. This will configure the behavior. - - >>> 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. - - >>> 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. - - >>> 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. - - >>> from plone.autoform.interfaces import IFormFieldProvider - >>> IFormFieldProvider.providedBy(tags_behavior.interface) - True +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 ------------------ @@ -107,52 +111,56 @@ Using the behavior Let's create a content object that has this behavior enabled and check that it works. - >>> doc = Document('doc') - >>> tags_adapter = ITags(doc, None) - >>> tags_adapter is not None - True +```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: - >>> doc.setSubject(['One', 'Two']) - >>> doc.Subject() - ('One', 'Two') +```pycon +>>> doc.setSubject(["One", "Two"]) +>>> doc.Subject() +("One", "Two") - >>> tags_adapter.tags == set(['One', 'Two']) - True +>>> tags_adapter.tags == set(["One", "Two"]) +True - >>> tags_adapter.tags = set(['Two', 'Three']) - >>> doc.Subject() == ('Two', 'Three') - 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 simply test the *Tags* adapter directly on a dummy context, but that is not terribly interesting, since all it does is convert sets to tuples. +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 *behaviors.zcml* file, which in this case will suffice. -We still need to include a minimal set of ZCML from Five. +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. +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. +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. +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. +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. In *tests.py*, we have: +To run the test, we need a test suite. +Here is our {file}`tests.py`. ```python from zope.app.testing import setup @@ -168,28 +176,28 @@ def tearDown(test): def test_suite(): return unittest.TestSuite(( doctest.DocFileSuite( - 'behaviors.rst', + "behaviors.rst", setUp=setUp, tearDown=tearDown, optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS), )) ``` -This runs the *behaviors.rst* doctest from the same directory as the *tests.py* file. -To run the test, we can use the usual test runner: +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. -``` -$ ./bin/instance test -s collective.gtags +```shell +./bin/instance test -s collective.gtags ``` -## Testing a dexterity type with a behavior + +## 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 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. -Here is the code: +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 -*- @@ -212,43 +220,43 @@ class RefreshBehaviorTestCase(unittest.TestCase): layer = INTEGRATION_TESTING def _enable_refresh_behavior(self): - fti = queryUtility(IDexterityFTI, name='collective.cover.content') + 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')) + notify(SchemaInvalidatedEvent("collective.cover.content")) def _disable_refresh_behavior(self): - fti = queryUtility(IDexterityFTI, name='collective.cover.content') + 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')) + notify(SchemaInvalidatedEvent("collective.cover.content")) def setUp(self): - self.portal = self.layer['portal'] - self.request = self.layer['request'] + self.portal = self.layer["portal"] + self.request = self.layer["request"] alsoProvides(self.request, ICoverLayer) - with api.env.adopt_roles(['Manager']): + with api.env.adopt_roles(["Manager"]): self.cover = api.content.create( - self.portal, 'collective.cover.content', 'c1') + 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()) + 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.assertIn("", view()) self.cover.ttl = 5 - self.assertIn('', view()) + self.assertIn("", view()) self._disable_refresh_behavior() - self.assertNotIn('', view()) + 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). @@ -258,7 +266,8 @@ To disable it, remove the behavior from the FTI behaviors: `behaviors.remove(IRe 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. +If you need a marker interface in a test, set it manually with `zope.interface.alsoProvides`, or write an integration test with Dexterity content.