Commit 37c3c7b6 authored by wenjie.zheng's avatar wenjie.zheng

Transition.py: Now generate workflow history for both Action and Workflow...

Transition.py: Now generate workflow history for both Action and Workflow Method, add Guard Object in real time, improve state getters.
parent 2973fc8a
##############################################################################
#
# Copyright (c) 2015 Nexedi SARL and Contributors. All Rights Reserved.
# 2015 Wenjie ZHENG <wenjie.zheng@tiolive.com>
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
Guard conditions in a erp5-configurable workflow.
"""
from cgi import escape
from AccessControl.SecurityInfo import ClassSecurityInfo
from Acquisition import Explicit
from Acquisition import aq_base
from App.class_init import InitializeClass
from App.special_dtml import DTMLFile
from Persistence import Persistent
from Products.CMFCore.Expression import Expression
from Products.CMFCore.utils import _checkPermission
from Products.DCWorkflow.Expression import StateChangeInfo
from Products.DCWorkflow.Expression import createExprContext
from Products.DCWorkflow.permissions import ManagePortal
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Type import Permissions, PropertySheet
class Guard(XMLObject):
permissions = ()
roles = ()
groups = ()
expr = None
meta_type = 'ERP5 Workflow'
portal_type = 'Guard'
add_permission = Permissions.AddPortalContent
isPortalContent = 1
isRADContent = 1
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
# Declarative properties
property_sheets = (
PropertySheet.Base,
PropertySheet.XMLObject,
PropertySheet.CategoryCore,
PropertySheet.DublinCore,
PropertySheet.Guard,
)
def check(self, sm, wf_def, ob, **kw):
"""Checks conditions in this guard.
"""
u_roles = None
if wf_def.manager_bypass:
# Possibly bypass.
u_roles = sm.getUser().getRolesInContext(ob)
if 'Manager' in u_roles:
return 1
if self.permissions:
for p in self.permissions:
if _checkPermission(p, ob):
break
else:
return 0
if self.roles:
# Require at least one of the given roles.
if u_roles is None:
u_roles = sm.getUser().getRolesInContext(ob)
for role in self.roles:
if role in u_roles:
break
else:
return 0
if self.groups:
# Require at least one of the specified groups.
u = sm.getUser()
b = aq_base( u )
if hasattr( b, 'getGroupsInContext' ):
u_groups = u.getGroupsInContext( ob )
elif hasattr( b, 'getGroups' ):
u_groups = u.getGroups()
else:
u_groups = ()
for group in self.groups:
if group in u_groups:
break
else:
return 0
expr = self.expr
if expr is not None:
econtext = createExprContext(
StateChangeInfo(ob, wf_def, kwargs=kw))
res = expr(econtext)
if not res:
return 0
return 1
security.declareProtected(ManagePortal, 'getSummary')
def getSummary(self):
# Perhaps ought to be in DTML.
res = []
if self.permissions:
res.append('Requires permission:')
res.append(formatNameUnion(self.permissions))
if self.roles:
if res:
res.append('<br/>')
res.append('Requires role:')
res.append(formatNameUnion(self.roles))
if self.groups:
if res:
res.append('<br/>')
res.append('Requires group:')
res.append(formatNameUnion(self.groups))
if self.expr is not None:
if res:
res.append('<br/>')
res.append('Requires expr:')
res.append('<code>' + escape(self.expr.text) + '</code>')
return ' '.join(res)
def changeFromProperties(self, props):
'''
Returns 1 if changes were specified.
'''
if props is None:
return 0
res = 0
s = props.get('guard_permissions', None)
if s:
res = 1
p = [ permission.strip() for permission in s.split(';') ]
self.permissions = tuple(p)
s = props.get('guard_roles', None)
if s:
res = 1
r = [ role.strip() for role in s.split(';') ]
self.roles = tuple(r)
s = props.get('guard_groups', None)
if s:
res = 1
g = [ group.strip() for group in s.split(';') ]
self.groups = tuple(g)
s = props.get('guard_expr', None)
if s:
res = 1
self.expr = Expression(s)
return res
security.declareProtected(ManagePortal, 'getPermissionsText')
def getPermissionsText(self):
if not self.permissions:
return ''
return '; '.join(self.permissions)
security.declareProtected(ManagePortal, 'getRolesText')
def getRolesText(self):
if not self.roles:
return ''
return '; '.join(self.roles)
security.declareProtected(ManagePortal, 'getGroupsText')
def getGroupsText(self):
if not self.groups:
return ''
return '; '.join(self.groups)
security.declareProtected(ManagePortal, 'getExprText')
def getExprText(self):
if not self.expr:
return ''
return str(self.expr.text)
InitializeClass(Guard)
def formatNameUnion(names):
escaped = ['<code>' + escape(name) + '</code>' for name in names]
if len(escaped) == 2:
return ' or '.join(escaped)
elif len(escaped) > 2:
escaped[-1] = ' or ' + escaped[-1]
return '; '.join(escaped)
...@@ -38,6 +38,12 @@ from Products.ERP5Type.Utils import convertToUpperCase, convertToMixedCase ...@@ -38,6 +38,12 @@ from Products.ERP5Type.Utils import convertToUpperCase, convertToMixedCase
from Products.DCWorkflow.DCWorkflow import ObjectDeleted, ObjectMoved from Products.DCWorkflow.DCWorkflow import ObjectDeleted, ObjectMoved
from copy import deepcopy from copy import deepcopy
from Products.ERP5Type.patches.WorkflowTool import WorkflowHistoryList from Products.ERP5Type.patches.WorkflowTool import WorkflowHistoryList
#from Products.ERP5Workflow.Document.Guard import Guard
from Products.DCWorkflow.Guard import Guard
TRIGGER_AUTOMATIC = 0
TRIGGER_USER_ACTION = 1
TRIGGER_WORKFLOW_METHOD = 2
class Transition(XMLObject): class Transition(XMLObject):
""" """
...@@ -49,6 +55,13 @@ class Transition(XMLObject): ...@@ -49,6 +55,13 @@ class Transition(XMLObject):
add_permission = Permissions.AddPortalContent add_permission = Permissions.AddPortalContent
isPortalContent = 1 isPortalContent = 1
isRADContent = 1 isRADContent = 1
trigger_type = TRIGGER_USER_ACTION #zwj: type is int 0, 1, 2
guard = None
actbox_name = ''
actbox_url = ''
actbox_icon = ''
actbox_category = 'workflow'
var_exprs = None # A mapping.
# Declarative security # Declarative security
security = ClassSecurityInfo() security = ClassSecurityInfo()
...@@ -63,6 +76,47 @@ class Transition(XMLObject): ...@@ -63,6 +76,47 @@ class Transition(XMLObject):
PropertySheet.Transition, PropertySheet.Transition,
) )
def getGuardSummary(self):
res = None
if self.guard is not None:
res = self.guard.getSummary()
return res
def getGuard(self):
if self.guard is not None:
return self.guard
else:
self.generateGuard()
return self.guard ### only generate gurad when self is a User Action
#return Guard().__of__(self) # Create a temporary guard.
def getVarExprText(self, id):
if not self.var_exprs:
return ''
else:
expr = self.var_exprs.get(id, None)
if expr is not None:
return expr.text
else:
return ''
def generateGuard(self):
if self.trigger_type == TRIGGER_USER_ACTION:
if self.guard == None:
self.guard = Guard(permissions=self.getPermissionList(),
roles=self.getRoleList(),
groups=self.getGroupList(),
expr=self.getExpression())
if self.guard.roles != self.getRoleList():
self.guard.roles = self.getRoleList()
elif self.guard.permissions != self.getPermissionList():
self.guard.permissions = self.getPermissionList()
elif self.guard.groups != self.getGroupList():
self.guard.groups = self.getGroupList()
elif self.guard.expr != self.getExpression():
self.guard.expr = self.getExpression()
def execute(self, document, form_kw=None): def execute(self, document, form_kw=None):
""" """
Execute transition. Execute transition.
...@@ -76,16 +130,22 @@ class Transition(XMLObject): ...@@ -76,16 +130,22 @@ class Transition(XMLObject):
if form_kw is None: if form_kw is None:
form_kw = {} form_kw = {}
workflow = self.getParentValue() workflow = self.getParentValue()
# Get variable values
### get related history
state_bc_id = workflow.getStateBaseCategory() state_bc_id = workflow.getStateBaseCategory()
status_dict = workflow.getCurrentStatusDict(document) status_dict = workflow.getCurrentStatusDict(document)
state_object = workflow._getOb(status_dict[state_bc_id]) state_object = workflow._getOb(status_dict[state_bc_id], None)
if state_object == None:
state_object = workflow.getSourceValue()
old_state = state_object.getId() old_state = state_object.getId()
new_state = document.unrestrictedTraverse(self.getDestination()).getId() old_sdef = state_object
new_state = self.getDestinationId()
if new_state is None: if new_state is None:
new_state = document.unrestrictedTraverse(workflow.getSource()).getId() new_state = workflow.getSourceId()
if not new_state: if not new_state:
# Do nothing if there is no initial state. We may want to create # Do nothing if there is no initial state. We may want to create
# workflows with no state at all, only for worklists. # workflows with no state at all, only for worklists.
...@@ -93,9 +153,9 @@ class Transition(XMLObject): ...@@ -93,9 +153,9 @@ class Transition(XMLObject):
former_status = {} former_status = {}
else: else:
former_status = state_object.getId() former_status = state_object.getId()
old_sdef = state_object
try: try:
new_sdef = document.unrestrictedTraverse(self.getDestination()) new_sdef = self.getDestinationValue()
except KeyError: except KeyError:
raise WorkflowException('Destination state undefined: ' + new_state) raise WorkflowException('Destination state undefined: ' + new_state)
...@@ -130,10 +190,15 @@ class Transition(XMLObject): ...@@ -130,10 +190,15 @@ class Transition(XMLObject):
if validation_exc : if validation_exc :
# reraise validation failed exception # reraise validation failed exception
raise validation_exc, None, validation_exc_traceback raise validation_exc, None, validation_exc_traceback
return new_sdef return old_state
# update state # update state
self._changeState(document) state = self.getDestination()
if state is None:
state = old_sdef
state_bc_id = self.getParentValue().getStateBaseCategory()
document.setCategoryMembership(state_bc_id, state)
### zwj: update Role mapping, also in Workflow, initialiseDocument() ### zwj: update Role mapping, also in Workflow, initialiseDocument()
self.getParent().updateRoleMappingsFor(document) self.getParent().updateRoleMappingsFor(document)
...@@ -141,7 +206,6 @@ class Transition(XMLObject): ...@@ -141,7 +206,6 @@ class Transition(XMLObject):
status_dict['action'] = self.getId() status_dict['action'] = self.getId()
# Modify workflow history # Modify workflow history
#status_dict[state_bc_id] = document.getCategoryMembershipList(state_bc_id)[0]
status_dict[state_bc_id] = new_state status_dict[state_bc_id] = new_state
object = workflow.getStateChangeInformation(document, state_object, transition=self) object = workflow.getStateChangeInformation(document, state_object, transition=self)
...@@ -163,21 +227,25 @@ class Transition(XMLObject): ...@@ -163,21 +227,25 @@ class Transition(XMLObject):
for variable in self.contentValues(portal_type='Transition Variable'): for variable in self.contentValues(portal_type='Transition Variable'):
status_dict[variable.getCausalityTitle()] = variable.getInitialValue(object=object) status_dict[variable.getCausalityTitle()] = variable.getInitialValue(object=object)
# Generate Workflow History List
self.setStatusOf(workflow.getId(), document, status_dict) self.setStatusOf(workflow.getId(), document, status_dict)
# Execute the "after" script. # Execute the "after" script.
script_id = self.getAfterScriptId() script_id = self.getAfterScriptId()
if script_id is not None: if script_id is not None:
kwargs = form_kw kwargs = form_kw
# Script can be either script or workflow method # Script can be either script or workflow method
if script_id in old_sdef.getDestinationTitleList(): if script_id in old_sdef.getDestinationIdList():
getattr(workflow, convertToMixedCase(script_id)).execute(document) getattr(workflow, convertToMixedCase(script_id)).execute(document)
else: else:
script = self.getParent()._getOb(script_id) script = self.getParent()._getOb(script_id)
# Pass lots of info to the script in a single parameter. # Pass lots of info to the script in a single parameter.
sci = StateChangeInfo( if script.getTypeInfo().getId() == 'Workflow Script':
document, workflow, former_status, self, old_sdef, new_sdef, kwargs) sci = StateChangeInfo(
script.execute(sci) # May throw an exception. document, workflow, former_status, self, old_sdef, new_sdef, kwargs)
script.execute(sci) # May throw an exception.
else:
raise NotImplementedError ('Unsupported Script %s for state %s'%(script_id, old_sdef.getId()))
# Return the new state object. # Return the new state object.
if moved_exc is not None: if moved_exc is not None:
# Propagate the notification that the object has moved. # Propagate the notification that the object has moved.
...@@ -185,16 +253,6 @@ class Transition(XMLObject): ...@@ -185,16 +253,6 @@ class Transition(XMLObject):
else: else:
return new_sdef return new_sdef
def _changeState(self, document):
"""
Change the state of the object.
"""
state = self.getDestination()
if state is not None:
# Some transitions don't update the state
state_bc_id = self.getParentValue().getStateBaseCategory()
document.setCategoryMembership(state_bc_id, state)
def _checkPermission(self, document): def _checkPermission(self, document):
""" """
Check if transition is allowed. Check if transition is allowed.
...@@ -227,6 +285,6 @@ class Transition(XMLObject): ...@@ -227,6 +285,6 @@ class Transition(XMLObject):
if wfh is None: if wfh is None:
wfh = WorkflowHistoryList() wfh = WorkflowHistoryList()
if not has_history: if not has_history:
ob.workflow_history = PersistentMapping() ob.workflow_history = PersistentMapping()
ob.workflow_history[wf_id] = wfh ob.workflow_history[wf_id] = wfh
wfh.append(status) wfh.append(status)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment