Creating Workflows Programmatically
This How-to applies to:
Any version.
This How-to is intended for:
Developers
If you are making a special product, you might want to have different workflows as the default workflows shipped with Plone. It's pain to recreate workflows manually each time installing the product - it's better to create workflows programmatically in the installation script of the product.
Here are few links which help you when you are starting to work with workflows.
- Plone Book chapter 8: Managing workflows
- dumpDCWorkflowScript - print out Plone's workflow repository
Here is a sample how to create simple two state workflow for Archetypes item Issue. Issue can be "in progress" or "sealed" and you need permission EDIT_APPLICATION_FOLDER_PERMISSION two switch between these states. The workflow is registered to portal_workflow tool.
Note that Plone 2.0.5 expects state variable to have name "review_state". More information about this quirk in this bug report
From install.py:
from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
from Products.DCWorkflow.Guard import Guard
from Products.CMFCore.WorkflowTool import addWorkflowFactory, _makeWorkflowFactoryKey
def createIssueWorkflow(id):
""" Creates Usability issue workflow for the portal
"""
flow=DCWorkflowDefinition(id)
# Install Issue workflow
flow.title = "Usability Issue Workflow"
# this var name is hardcoded in global_defines.pt
flow.variables.setStateVar('review_state')
flow.initial_state = "inprogress"
flow.states.addState('inprogress')
inprogress = flow.states["inprogress"]
inprogress.title = "In Progress"
inprogress.description = "Users are allowed to contribute for this item"
inprogress.transitions = ("seal",)
# Only users with management permissions can do state transitions
transitionGuard = Guard()
transitionGuard.permissions = (MANAGE_USABILITY_ITEMS_PERMISSION,)
flow.states.addState("sealed")
sealed = flow.states["sealed"]
sealed.title = "Sealed"
sealed.description = "Item has been closed. It is either a duplicate or fixed."
sealed.transitions = ("reopen",)
flow.transitions.addTransition("seal")
seal = flow.transitions["seal"]
seal.title = "Seal"
seal.actbox_name = "Seal"
seal.description = "Make item sealed"
seal.new_state_id = "sealed"
seal.guard = transitionGuard
seal.actbox_url='%(content_url)s/content_seal_form'
flow.transitions.addTransition("reopen")
reopen = flow.transitions["reopen"]
reopen.title = "Reopen"
reopen.actbox_name = "Reopen"
reopen.description = "Reopen item for contribution"
reopen.new_state_id = "inprogress"
reopen.guard = transitionGuard
reopen.actbox_url='%(content_url)s/content_reopen_form'
# In inprogress state, all members can edit the item. In sealed state,
# it is only possible for managers.
# No one has EDIT_USABILITY_ITEMS_PERMISSION set in any other point of code,
# it is set only here.
flow.permissions+=(EDIT_USABILITY_ITEMS_PERMISSION, )
# Member gains back its permissions
flow.states.inprogress.setPermission(EDIT_USABILITY_ITEMS_PERMISSION, False,
('Member','Manager','Owner',USABILITY_MANAGER_ROLE,USABILITY_EDITOR_ROLE,))
return flow
def setupWorkflows(portal, out):
flow_id = "issue_workflow"
flow_title = "Usability Issue Workflow"
addWorkflowFactory(createIssueWorkflow, id=flow_id, title=flow_title)
workflowTool = portal.portal_workflow
workflowTool.setChainForPortalTypes("Issue", flow_id)
workflow_type=_makeWorkflowFactoryKey(createIssueWorkflow, id=flow_id, title=flow_title)
workflowTool.manage_addWorkflow(id=flow_id, workflow_type=workflow_type)
out.write("Succesfully installed issue_workflow\n")
portal.portal_catalog.refreshCatalog()
workflowTool.updateRoleMappings()
Then some unit testing example code, how to test this workflow:
from Products.CMFCore.Expression import Expression
from Products.PageTemplates.Expressions import getEngine
def executeUntrustedExpression(condition, mappings):
""" A helper function to run code with restricted rights
Emulates evaluating TALES expressions
@param condition TALES expression to evaluate, e.g. "python: object.setTitle('blaa')"
@param mappings Dictionary of available local variables for expressions, e.g.:
data = {
'object': object,
'folder': folder,
'portal': portal,
'nothing': None,
'request': getattr( object, 'REQUEST', None ),
'modules': SecureModuleImporter,
'member': member,
}
"""
ec = getEngine().getContext(mappings)
return Expression(condition)(ec)
def test_05_workflow(self):
""" Check that we can switch between workflow states and sealed items cannot be changed
"""
root = self.portal
workflowTool = root.portal_workflow
# Login as editor
self.login("editor")
# Create application folder
root.invokeFactory(type_name='ApplicationFolder', id="appfolder")
appfolder = root.appfolder
# now switch to normal member
self.logout()
user = self.login("user1")
# Create a application
appfolder.invokeFactory(type_name = 'Application', id="app")
# Create issue
appfolder.app.invokeFactory(type_name="Issue", id="issue")
flow = workflowTool.getWorkflowsFor(appfolder.app.issue)[0]
self.failUnless(flow.id == "issue_workflow")
# sealing items by user shoudln't be allowed
try:
workflowTool.doActionFor(appfolder.app.issue, "seal")
self.fail("Normal user was able to seal item")
except:
pass
#dumpRolesAndPermissions(getSecurityManager().getUser(), appfolder.app.issue)
self.logout()
self.login("editor")
# Ensure we have Seal and Reopen actions listed in UI workflow menu
actions = workflowTool.getActionsFor(appfolder.app.issue)
self.failUnless(len(actions) == 1)
self.failUnless(actions[0]["name"] == "Seal", "No Seal action listed for UI")
# Check also that we have action available through action tool
actionTool = root.portal_actions
actions = actionTool.listFilteredActionsFor(appfolder.app.issue)
self.failUnless(actions["workflow"][0]["name"] == "Seal", "No Seal action listed for UI")
# Check that Plone UI receives our state variable
# TODO: review_state variable is hard coded to global_defines.pt in Plone 2.0.5
# Other state varibles don't work. This might have been fixed in later versions.
info = workflowTool.getInfoFor(appfolder.app.issue, 'review_state', None);
self.failUnless(info != None, "Plone 2.0.5 uses hard coded state variable review_state")
# Seal test item
workflowTool.doActionFor(appfolder.app.issue, "seal")
# Ensure we have Seal and Reopen actions listed in UI workflow menu
actions = workflowTool.getActionsFor(appfolder.app.issue)
self.failUnless(len(actions) == 1)
self.failUnless(actions[0]["name"] == "Reopen", "No Reopen action listed for UI")
# Normal user shouldn't be allowed to change values when the item is sealed
self.logout()
self.login("user1")
#print "after sealing"
#dumpRolesAndPermissions(getSecurityManager().getUser(), appfolder.app.issue)
#dumpRolesAndPermissions(getSecurityManager().getUser(), appfolder.app)
#dumpRolesAndPermissions(getSecurityManager().getUser(), appfolder)
#dumpRole("Anonymous", appfolder.app.issue)
data = { "issue" : appfolder.app.issue }
try:
executeUntrustedExpression("python: issue.setIssueDescription('bloo')", data)
self.fail("Member user was able to edit sealed item")
except Unauthorized,e:
pass
try:
executeUntrustedExpression("python: issue.setTitle('bloo')", data)
self.fail("Member user was able to edit sealed item")
except Unauthorized,e:
pass
self.logout()
self.login("editor")
workflowTool.doActionFor(appfolder.app.issue, "reopen")
# Ensure that we propeply returned to initial in-progress state
actions = workflowTool.getActionsFor(appfolder.app.issue)
self.failUnless(len(actions) == 1)
self.failUnless(actions[0]["name"] == "Seal", "No Seal action listed for UI")
self.logout()
self.login("user1")
# now user should be able to edit item again
appfolder.app.issue.setTitle("whooo")
# Try seal reopened item
self.logout()
self.login("editor")
workflowTool.doActionFor(appfolder.app.issue, "seal")
Setting workflow scripts
Workflow scripts need to be directly uploaded to Zope database and as far as I know they cannot exist as file system files.
Here is how the trick is done:
SET_AD_NUMBER_SCRIPT = """
##parameters=state_change
# Set ad number for property ad during publishing
from Products.CMFCore.utils import getToolByName
object = state_change.object
adnumber_tool = getToolByName(object, 'retypes_adnumber_tool')
object.setAdNumber(adnumber_tool.nextId())
"""
def addWorkflowScript(workflow, scriptName, script):
if hasattr(workflow.scripts, scriptName):
delattr(worflow.scripts, scriptName)
manage_addPythonScript(workflow.scripts, id=scriptName)
wf_script = getattr(workflow.scripts, scriptName)
wf_script.ZPythonScript_edit(None, script)
addWorkflowScript(portal.portal_workflow.property_workflow, "setAdNumberScript", SET_AD_NUMBER_SCRIPT)