ftw.lawgiver generates Plone workflows based on a human readable
specification written in a custom
Table of Contents
- How it works
- Action groups
- The workflow specification
- Generating the workflow
- Testing the workflow
- Customizing the sharing view
- Intercept and customize transitions
- Rebuild with console
Developing and maintaining complex Plone workflows is a time-consuming and cumbersome endeavor. Dozens of permissions need to be managed for different roles and different workflow states. Usually, this has to be done directly in the ZMI of Zope by selecting or unselecting thousands of checkboxes. This process has been shown to be very tedious and prone to errors. Furthermore, it is no simple task to document the workflow and the associated design decisions which led to the resulting configuration of permissions and roles. The extension or adaption of an existing workflow becomes very difficult, leading to workflows which are barely maintainable.
Another problem is the communication between workflow integrator and customer. The security system of Zope is based on a role-based access control (RBAC) which is intrinsically complex due to its use of roles, permissions, and workflow states. Experience has shown that these security concepts can be hard to convey to customers.
ftw.lawgiver helps solving these problems by using a DSL to describe how
a workflow should work. The lawgiver then generates the complete workflow
definition.xml) based on this specification. By separating this
specification from the resulting workflow definition (which is in XML) the
specification does not have to use permissions--handling the permissions is the
job of the lawgiver.
Using the specification file the workflow can easily be regenerated at any time
and will handle additional permissions automatically when regenerated. However,
it is still the developer's task to regenerate the
more or other permissions have to be managed. He or she has to make sure that
the workflow is properly installed with an upgrade step / reindexing security.
ftw.lawgiverto your buildout configuration:
[instance] eggs += ftw.lawgiver
- Install the generic setup profile of
In the specification we use the concept of so called action groups for describing what a role is allowed to do. It basically groups together a bunch of semantically similar Plone / Zope permissions so that we only have to define the workflow based on these action groups and not on individual permissions.
For example there is a
view action group which contains permissions such
Access Contents Information.
The registration of a permission to an action group should be done in the
package where the permission is defined. This allows to keep changes of the
permissions and action group registrations together in branches, for reviews
ftw.lawgiver already assigns default Plone / Zope permissions to action
The registration is done in ZCML.
Here is an example
<configure xmlns="http://namespaces.zope.org/zope" xmlns:lawgiver="http://namespaces.zope.org/lawgiver" i18n_domain="my.package"> <include package="ftw.lawgiver" /> <lawgiver:map_permissions action_group="add" permissions="my.package: Add Foo, my.package: Add Bar" /> </configure>
If you define multiple permissions in the same map_permissions directive make sure to separate them by comma.
By putting the ZCML in a separate
lawgiver.zcml file you can define
lawgiver in your addon package without having to define a dependency on
ftw.lawgiver by using
zcml:condition while loading it in your default
<configure xmlns="http://namespaces.zope.org/zope" xmlns:zcml="http://namespaces.zope.org/zcml" i18n_domain="my.package"> <include zcml:condition="installed ftw.lawgiver" file="lawgiver.zcml" /> </configure>
Maybe the permission to action group mapping does not work well for a specific workflow and you would like to change to mapping for this workflow only.
This can be easily achieved by also defining the workflow in the ZCML:
<configure xmlns="http://namespaces.zope.org/zope" xmlns:lawgiver="http://namespaces.zope.org/lawgiver" i18n_domain="my.package"> <include package="ftw.lawgiver" /> <lawgiver:map_permissions action_group="add" permissions="my.package: Add Foo, my.package: Add Bar" workflow="my_workflow" /> </configure>
Sometimes, a permission should be assigned to multiple action groups.
This can be done with the
move attribute of the
Just make sure that all other
map_permissions ZCMLs are loaded before doing that,
especially the default
<configure xmlns="http://namespaces.zope.org/zope" xmlns:lawgiver="http://namespaces.zope.org/lawgiver" i18n_domain="my.package"> <include package="ftw.lawgiver" /> <lawgiver:map_permissions action_group="add" permissions="Add portal content" /> <lawgiver:map_permissions action_group="add ticket" permissions="my.package: Add Ticket" workflow="my_workflow" /> <!-- We want to have "Add portal content" in the "add ticket" action group too, but we should not remove it from "add". By using move="False" we can add "Add portal content" to "add ticket" without removing it from "add". /--> <lawgiver:map_permissions action_group="add ticket" permissions="Add portal content" workflow="my_workflow" move="False" /> </configure>
lawgiver:workflow directive can be used to group multiple statements and
apply them to a specific workflow.
<configure xmlns="http://namespaces.zope.org/zope" xmlns:lawgiver="http://namespaces.zope.org/lawgiver" i18n_domain="my.package"> <include package="ftw.lawgiver" /> <lawgiver:workflow name="the-workflow"> <lawgiver:map_permissions action_group="add folder" permissions="Add folder" /> <lawgiver:ignore permissions="ATContentTypes: View history" /> </lawgiver:workflow> </configure>
The specification is written in a plain text file (
the same directory where the
definition.xml is saved.
The states and transitions are defined in simple lists:
[My Custom Workflow] Description: A three state publication workflow Initial Status: Private Status Private: Status Pending: Status Published: Transitions: Publish (Private => Published) Submit for publication (Private => Pending) Reject (Pending => Private) Retract (Pending => Private) Publish (Pending => Published) Reject (Published => Private)
We are not using any internal ids for workflow states or
transitions. Instead, we use the same labels which the user will actually
see--the ids are automatically generated by
In Plone we have a given set of rather technical roles (e.g. Editor, Contributor, Reader) which may not apply for all use cases in real life. The customer may have own roles with different names. Since the existing roles are already well established in Plone it is usually not a good thing to add new roles to Plone. It is better to try to reuse the existing roles.
Because the customer has different labels for his roles we need to map customer roles to Plone roles:
Role mapping: editor-in-chief => Reviewer editor => Editor everyone => Anonymous
In our example we have only "normal" editors and an "editor-in-chief" who can review and publish the contents. We do not have to use the Contributor role since our editors can edit, add new content, and request a review for existing content. Therefore, it is not necessary to distinguish Editor and Contributor role.
Transition-guard expressions is a way to hide your transitions dynamically, in addition to the guard-roles. Use the options-syntax to define a guard-expression.
Expressions in DCWorkflow are TALES expressions. To see the contexts available in expressions, take a look at portal_workflow/[your-workflow-id]/guardExprDocs
Warning: Transition-guard expressions do not protect the transition itself. If the user knows the URL to perform the transition, it will pass. It only hides the transition from the user.
[My Custom Workflow] Initial Status: Private Status Private: Status Published: Transitions: Publish (Private => Published) [guard-expression => python:here.guard(state_change)] Reject (Published => Private) [guard-expression => here/guard_reject]
Usually there are some general statements, for example that a user with the Adminstrator role can always edit the contents in any workflow state. Such statements should not be repeated for every state but defined once as a general statement.
General: An administrator can always view the content An administrator can always edit the content An administrator can always delete the content
These general statements apply for all states.
For each state we describe the actions a user with a certain role can perform. We follow the principle that any user / role is NOT allowed do anything by default, we have to explicitly list every action he will be allowed to perform.
Status Private: An editor can view this content. An editor can edit this content. An editor can delete this content. An editor can add new content. An editor can submit for publication. An editor-in-chief can view this content. An editor-in-chief can edit this content. An editor-in-chief can delete this content. An editor-in-chief can add new content. An editor-in-chief can publish this content. Status Pending: An editor can view this content. An editor can add new content. An editor can retract this content. An editor-in-chief can view this content. An editor-in-chief can edit this content. An editor-in-chief can delete this content. An editor-in-chief can add new content. An editor-in-chief can publish this content. An editor-in-chief can reject this content. Status Published: An editor can view this content. An editor can add new content. An editor can retract this content. An editor-in-chief can view this content. An editor-in-chief can add new content. An editor-in-chief can retract this content. Anyone can view this content.
Roles can be inherited from other roles, globally and for a single status:
[Role Inheritance Workflow] Initial Status: Foo Role mapping: editor => Editor editor-in-chief => Reviewer administrator => Site Administrator General: An administrator can always perform the same actions as an editor. An administrator can always perform the same actions as an editor-in-chief. Status Foo: An editor-in-chief can perform the same actions as an editor. An editor can view this content. An editor can edit this content. Status Bar: An editor can view this content. An editor-in-chief can view this content. An editor-in-chief can edit this content.
Worklists are automatically generated for you when you grant access to the worklist:
[A workflow] ... Status Pending: An editor-in-chief can access the worklist.
Those "can access the worklist" statements do not work in the "General" section, they need to be defined a "Status" section.
For each status with "can access the worklist" statements a worklist is generated, guarded with the role for which there is a statement.
All workflow directories in registered generic setup profiles
are automatically scanned for workflow specifications.
Just place a
specification.txt in a workflow directory and
will discover it automatically.
- Workflow XML:
In this example it is assumed that
profiles/default is a registered generic setup
Sometimes the transition URLs need to point to another view. This can be
achieved by using the
transition-url option, where a string can be passed
which will then be substituted with the
transition id. Be sure to use a
%% for parts which should not be replaced when generating the workflow,
such as the
transition-url = %%(content_url)s/custom_wf_action?workflow_action=%(transition)s
Currently supported languages:
- Example: ftw/lawgiver/tests/assets/languages/specification.txt
- Example: ftw/lawgiver/tests/assets/languages/specification.de.txt
Contributing new languages
We happily accept pull requests with new languages!
Creating a new language is as simple:
- Create a new specification example in
ftw/lawgiver/tests/assets/languages/, implementing the same workflow as
- Run the tests with
bin/test. It should fail at this point. Keep running them after each change.
- Add a new language module to
- Register the new language in
- Implement the language specific constraints and extraction methods in your new language class until all tests pass.
- Add the language to the readme.
- Send us a pull request!
For generating the workflow go to the lawgiver control panel (in the Plone control panel). There you can see a list of all workflows and by selecting one you can see the specification and other details, such as the action groups.
On this view you can generate the workflow (automatically saved to the
definition.xml in the same directory as the
specification.txt) and you
can install the workflow / update the security.
Update translations in locales directory in the workflow
details view helps you keep your translations up to date.
It writes directly to the locales directory on your machine.
When updating the translations, these files are written:
When updating the messages in your locales file, all no longer valid messages which start with the workflow ID prefix are removed automatically.
It is important to detect when you have to rebuild your workflow. It is also important to detect permissions from third party addons which are not yet mapped to action groups.
By subclassing the WorkflowTest it is easy to write a test for your workflow:
from ftw.lawgiver.tests.base import WorkflowTest from my.package.testing import MY_INTEGRATION_TESTING class TestMyWorkflow(WorkflowTest): # The workflow path may be a path relative to the this file or # an absolute path. workflow_path = '../profiles/default/workflows/my-workflow' # Use an integration testing layer. layer = MY_INTEGRATION_TESTING
What is tested?
- The test will fail when your workflow (
definition.xml) needs to be regenerated. This may be because new permissions should be managed.
- The test will fail when you install new addons which provide new permissions. The permissions should be mapped to action groups or marked as unmanaged explicitly:
<configure xmlns="http://namespaces.zope.org/zope" xmlns:lawgiver="http://namespaces.zope.org/lawgiver" i18n_domain="ftw.lawgiver"> <include package="ftw.lawgiver" /> <lawgiver:ignore workflow="my_workflow" permissions="ATContentTypes: Upload via url, ATContentTypes: View history" /> </configure>
Lawgiver allows you to customize the sharing view to your needs.
By default the
@@sharing view lists some default Plone roles:
- Can add (
- Can edit (
- Can review (
- Can view (
Often the workflow does not use all of those roles, or uses different ones.
Lawgiver allows you to configure which roles are showing up in at the
view. If your users are granting roles on the
@@sharing view, you should probably
configure the roles so that they have meanigful names and only the relevant ones
If you want to customize the displayed roles for your workflow, you can do this right in your workflow specification:
[A workflow] Role mapping: editor => Editor editor-in-chief => Reviewer administrator => Site Administrator Visible roles: editor editor-in-chief
The lawgiver then sets the permissions required for managing a role correctly.
This works for registered roles. Plone only registers
Reader by default.
See the Registering additional roles section.
The lawgiver extends Plone's role translation system so that the
roles in the
@@sharing view can be translated per workflow.
This is done through the Plone standard role utilites, allowing addon tools to also use the corrent role translation without the need of customization.
The lawgiver provides example translations (
the lawgiver control panel, which can easily be copied to your local plone
locales). These translations also include role translations
and can be modified to your needs.
The lawgiver automatically looks up the right translation of the roles, depending on your workflow.
You can easily register custom roles or Plone default roles which are not visible
by default (such as
Use the lawgiver directive for registering new roles:
<configure xmlns="http://namespaces.zope.org/zope" xmlns:lawgiver="http://namespaces.zope.org/lawgiver" i18n_domain="my.package"> <include package="ftw.lawgiver" /> <lawgiver:role name="Site Manager" /> </configure>
lawgiver:role directive does all the required things for you, such as
registering the permission in zope, mapping the permission to the default
manage security action group and registering the required utility
permission: the required permission for granting this role. The permission is automatically generated as
Sharing page: Delegate [ROLE] role.
register_permission: automatically registers the permissions in Zope. This is
map_permission: automatically map the permission to the default lawgiver
manage securityaction group. Lawgiver will also re-map the permission according to your
Visible rolesconfiguration in the workflow specification.
ftw.lawgiver automatically registers an overlay when clicking on the
role text in the table header on the sharing view.
The overlay displays a description of what this role can do in each state of
the current workflow:
You can add text to the overlay per role directly in your workflow specification:
[A workflow] Role mapping: editor => Editor editor-in-chief => Reviewer administrator => Site Administrator editor-in-chief role description: The editor-in-chief reviews and publishes content.
This text is included as translation proposal for the
plone domain, which
makes it easy to translate it to other languages for multilingual sites.
Sometimes we need to change the behavior when executing certain transitions.
ftw.lawgiver provides a base view class
ModifyStatusViewBase for making
such enhancements easier, combined with the
transition-url option in the
The idea is that a custom view is implemented, subclassing
The view can be implemented for one workflow or for multiple workflows in the same project.
The view allows to easily intercept certain transitions, changing, enhancing or aborting
the standard behavior.
Implement a custom view with custom behavior:
from ftw.lawgiver.browser.modifystatus import ModifyStatusViewBase class WebModifyStatus(ModifyStatusViewBase): @ModifyStatusViewBase.intercept('web--TRANSITION--publish--draft-published') def verify_on_publish(self, context, transition): # verify things return self.redirect_to_content_status_modify(context, transition)
The default behavior for not intercepted transitions is to redirect to the default
content_status_modifyscript, which is the default behavior of Plone. The default behavior is implemented with the
redirect_to_content_status_modify, so the example above also falls back to the default behavior.
The base class provides functionality as methods:
execute_transition(context, transition, **kwargs): executes the
content_status_modifyscript in-line, so that we can later change the redirect or do more things in the same transaction.
redirect_to_content_status_modify(context, transaction): redirects the browser the the
content_status_modifyscript. The script is not executed in the same transaction.
set_workflow_state(context, review_state, **infos): changes the workflow state of the context without executing a transition or respecting any guards.
in_state(context, review_state): context manager for temporarily switching the review state.
redirect_to(context): redirects to the absolute url of the context.
Register the view in ZCML:
<browser:page name="web-modify-status" for="*" class=".modifystatus.WebModifyStatus" permission="zope2.View" />
Configure the lawgiver workflow to use the this view as action:
# Settings transition-url = %%(content_url)s/@@web-modify-status?transition=%(transition)s
collective.deletepermission solves a delete problem, which occurs in certain situations, by adding a new delete permission. See its readme for further details.
For being able to delete content, the user should have the
Delete portal content) on the content but also
on the parent content.
This package provides an uninstall Generic Setup profile, however, it will not uninstall the package dependencies. Make sure to uninstall the dependencies if you no longer use them.
ftw.lawgiver registers a zopectl command so that all workflows can be
rebuilt at once using the console:
$ ./bin/instance rebuild_workflows --help usage: interpreter [-h] [-s SITE] Rebuild ftw.lawgiver workflows. optional arguments: -h, --help show this help message and exit -s SITE, --site SITE Path to the Plone site for discovering the worklfows. (default: Plone)
- Github: https://github.com/4teamwork/ftw.lawgiver
- Issues: https://github.com/4teamwork/ftw.lawgiver/issues
- Pypi: http://pypi.python.org/pypi/ftw.lawgiver
- Continuous integration: https://jenkins.4teamwork.ch/search?q=ftw.lawgiver
This package is copyright by 4teamwork.
ftw.lawgiver is licensed under GNU General Public License, version 2.