How to declare additional workflows for a behavior#

This guide shows you how to make a behavior contribute one or more workflows to the chain of every content type that enables it.

Prerequisites#

  • An add-on package with its own configure.zcml.

  • A workflow to contribute, installed by a GenericSetup profile. To write one that composes safely, see How to write a workflow that composes.

1. Define the marker interface#

Your behavior's marker interface must extend IAdditionalWorkflows.

from collective.multiworkflow.interfaces import IAdditionalWorkflows


class IFoundationMember(IAdditionalWorkflows):
    """Marker for content whose membership status is tracked."""

That base marker is the single registration point for the chain adapter. A marker that does not extend it never reaches the adapter, and the declaration in step 3 refuses to configure.

2. Register the behavior#

Register the marker as a behavior in the usual way.

<configure xmlns:plone="http://namespaces.plone.org/plone">

  <plone:behavior
      name="my.package.foundation_member"
      title="Foundation member"
      description="Track a membership lifecycle alongside publication."
      provides=".interfaces.IFoundationMember"
      />

</configure>

No factory is needed. With none, plone.behavior applies the interface to content as a marker, which is all the mechanism requires.

3. Declare the contribution#

<plone:additionalworkflows
    marker=".interfaces.IFoundationMember"
    workflows="foundation_member_workflow"
    />

To contribute several workflows, separate their ids with whitespace. They are appended in the order given, after the workflows the content type is already configured with.

To show the workflow under a name other than its title, give it a label.

<plone:additionalworkflows
    marker=".interfaces.IFoundationMember"
    workflows="foundation_member_workflow"
    label="Foundation membership"
    />

The label is translated in the i18n_domain of your ZCML file, so add it to your package's message catalogs like any other string. A directive with a label must name exactly one workflow; declare each labelled workflow in a directive of its own.

See ZCML directive for the directive's full description.

4. Enable the behavior on a type#

Enable it as you would any behavior, in the type's FTI.

<property name="behaviors">
  <element value="my.package.foundation_member" />
</property>

Every object of that type now resolves a chain with your workflow appended.

If the type contributes workflows from more than one behavior, Order of contributions gives the order they are appended in.

5. Bring existing content up to date#

Skip this step if the type has no content yet.

Existing objects of the type join the chain at once and read the contributed workflow's initial state. They are not reindexed, though, and the workflow's permission map is not applied to them, so do both.

from plone import api

catalog = api.portal.get_tool("portal_catalog")
catalog.reindexIndex("workflow_states", None)

wftool = api.portal.get_tool("portal_workflow")
wftool.updateRoleMappings()

Both calls walk the whole site, so run them once after enabling the behavior, from an upgrade step or a maintenance script. updateRoleMappings changes nothing unless the contributed workflow manages permissions.

Register from Python instead#

Use contributes where ZCML cannot express the declaration—building a contribution from configuration read at start-up, for instance.

from collective.multiworkflow.declaration import contributes
from collective.multiworkflow.interfaces import IAdditionalWorkflowsFor
from zope.component import getGlobalSiteManager

factory = contributes(IFoundationMember, "foundation_member_workflow")
getGlobalSiteManager().registerSubscriptionAdapter(
    factory, (IFoundationMember,), IAdditionalWorkflowsFor
)

Register it as a subscription adapter, as shown. A plain adapter has one winner per interface, so content providing two participating behaviors would gain the workflows of only one of them.

Verify#

Create an object of the type and read its chain.

from plone import api

wftool = api.portal.get_tool("portal_workflow")
chain = wftool.getChainFor(obj)

assert chain == (
    "simple_publication_workflow",
    "foundation_member_workflow",
)

If your workflow is missing from the chain, check the instance log. A contributed id that no workflow answers to is logged and skipped, so that a bad declaration cannot break chain lookup and make the object unusable.

See also

About behavior-driven assignment for why the mechanism is built this way.