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 View,
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
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 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
Secondary reviewers group
have the 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 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 [*], 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.
[*] | An External Method is a Python script evaluated in Zope context. See Logic Objects 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
<includeDependencies
/>
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 version="1.0"?>
<object name="portal_workflow">
<bindings>
<type type_id="example.conference.session">
<bound-workflow workflow_id="example.conference.session_workflow"/>
</type>
</bindings>
</object>
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
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 Sharing tab to grant Can add to the Authenticated Users group. This grants the Contributor local role to members. By default, this role is granted the Add portal content permission.