vendor/CMF/1.6.1/DCWorkflow

view DCWorkflow.py @ 0:238bab7e7116

CMF 1.6.1 vendor import
author fguillaume
date Tue, 13 Jun 2006 14:57:59 +0000
parents
children
line source
1 ##############################################################################
2 #
3 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
4 #
5 # This software is subject to the provisions of the Zope Public License,
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10 # FOR A PARTICULAR PURPOSE.
11 #
12 ##############################################################################
13 """ Web-configurable workflow.
15 $Id: DCWorkflow.py 68176 2006-05-18 15:55:57Z tseaver $
16 """
18 # Zope
19 from AccessControl import ClassSecurityInfo
20 from AccessControl import getSecurityManager
21 from AccessControl import Unauthorized
22 from Acquisition import aq_inner
23 from Acquisition import aq_parent
24 from App.Undo import UndoSupport
25 from DocumentTemplate.DT_Util import TemplateDict
26 from Globals import InitializeClass
27 from OFS.Folder import Folder
28 from OFS.ObjectManager import bad_id
29 from zope.interface import implements
31 # CMFCore
32 from Products.CMFCore.interfaces import IWorkflowDefinition
33 from Products.CMFCore.interfaces.portal_workflow \
34 import WorkflowDefinition as z2IWorkflowDefinition
35 from Products.CMFCore.utils import getToolByName
36 from Products.CMFCore.WorkflowCore import ObjectDeleted
37 from Products.CMFCore.WorkflowCore import ObjectMoved
38 from Products.CMFCore.WorkflowCore import WorkflowException
39 from Products.CMFCore.WorkflowTool import addWorkflowFactory
41 # DCWorkflow
42 from interfaces import IDCWorkflowDefinition
43 from permissions import ManagePortal
44 from utils import _dtmldir
45 from utils import modifyRolesForPermission
46 from utils import modifyRolesForGroup
47 from WorkflowUIMixin import WorkflowUIMixin
48 from Transitions import TRIGGER_AUTOMATIC
49 from Transitions import TRIGGER_USER_ACTION
50 from Transitions import TRIGGER_WORKFLOW_METHOD
51 from Expression import StateChangeInfo
52 from Expression import createExprContext
55 def checkId(id):
56 res = bad_id(id)
57 if res != -1 and res is not None:
58 raise ValueError, 'Illegal ID'
59 return 1
62 class DCWorkflowDefinition (WorkflowUIMixin, Folder):
63 '''
64 This class is the workflow engine and the container for the
65 workflow definition.
66 UI methods are in WorkflowUIMixin.
67 '''
69 implements(IDCWorkflowDefinition, IWorkflowDefinition)
70 __implements__ = z2IWorkflowDefinition
72 title = 'DC Workflow Definition'
73 _isAWorkflow = 1
75 state_var = 'state'
76 initial_state = None
78 states = None
79 transitions = None
80 variables = None
81 worklists = None
82 scripts = None
84 permissions = ()
85 groups = () # Names of groups managed by this workflow.
86 roles = None # The role names managed by this workflow.
87 # If roles is None, listRoles() provides a default.
89 creation_guard = None # The guard that can veto object creation.
91 manager_bypass = 0 # Boolean: 'Manager' role bypasses guards
93 manage_options = (
94 {'label': 'Properties', 'action': 'manage_properties'},
95 {'label': 'States', 'action': 'states/manage_main'},
96 {'label': 'Transitions', 'action': 'transitions/manage_main'},
97 {'label': 'Variables', 'action': 'variables/manage_main'},
98 {'label': 'Worklists', 'action': 'worklists/manage_main'},
99 {'label': 'Scripts', 'action': 'scripts/manage_main'},
100 {'label': 'Permissions', 'action': 'manage_permissions'},
101 {'label': 'Groups', 'action': 'manage_groups'},
102 )
104 security = ClassSecurityInfo()
105 security.declareObjectProtected(ManagePortal)
107 def __init__(self, id):
108 self.id = id
109 from States import States
110 self._addObject(States('states'))
111 from Transitions import Transitions
112 self._addObject(Transitions('transitions'))
113 from Variables import Variables
114 self._addObject(Variables('variables'))
115 from Worklists import Worklists
116 self._addObject(Worklists('worklists'))
117 from Scripts import Scripts
118 self._addObject(Scripts('scripts'))
120 def _addObject(self, ob):
121 id = ob.getId()
122 setattr(self, id, ob)
123 self._objects = self._objects + (
124 {'id': id, 'meta_type': ob.meta_type},)
126 #
127 # Workflow engine.
128 #
130 def _getStatusOf(self, ob):
131 tool = aq_parent(aq_inner(self))
132 status = tool.getStatusOf(self.id, ob)
133 if status is None:
134 return {}
135 else:
136 return status
138 def _getWorkflowStateOf(self, ob, id_only=0):
139 tool = aq_parent(aq_inner(self))
140 status = tool.getStatusOf(self.id, ob)
141 if status is None:
142 state = self.initial_state
143 else:
144 state = status.get(self.state_var, None)
145 if state is None:
146 state = self.initial_state
147 if id_only:
148 return state
149 else:
150 return self.states.get(state, None)
152 def _getPortalRoot(self):
153 return aq_parent(aq_inner(aq_parent(aq_inner(self))))
155 security.declarePrivate('getCatalogVariablesFor')
156 def getCatalogVariablesFor(self, ob):
157 '''
158 Allows this workflow to make workflow-specific variables
159 available to the catalog, making it possible to implement
160 worklists in a simple way.
161 Returns a mapping containing the catalog variables
162 that apply to ob.
163 '''
164 res = {}
165 status = self._getStatusOf(ob)
166 for id, vdef in self.variables.items():
167 if vdef.for_catalog:
168 if status.has_key(id):
169 value = status[id]
171 # Not set yet. Use a default.
172 elif vdef.default_expr is not None:
173 ec = createExprContext(StateChangeInfo(ob, self, status))
174 value = vdef.default_expr(ec)
175 else:
176 value = vdef.default_value
178 res[id] = value
179 # Always provide the state variable.
180 state_var = self.state_var
181 res[state_var] = status.get(state_var, self.initial_state)
182 return res
184 security.declarePrivate('listObjectActions')
185 def listObjectActions(self, info):
186 '''
187 Allows this workflow to
188 include actions to be displayed in the actions box.
189 Called only when this workflow is applicable to
190 info.object.
191 Returns the actions to be displayed to the user.
192 '''
193 ob = info.object
194 sdef = self._getWorkflowStateOf(ob)
195 if sdef is None:
196 return None
197 res = []
198 for tid in sdef.transitions:
199 tdef = self.transitions.get(tid, None)
200 if tdef is not None and tdef.trigger_type == TRIGGER_USER_ACTION:
201 if tdef.actbox_name:
202 if self._checkTransitionGuard(tdef, ob):
203 res.append((tid, {
204 'id': tid,
205 'name': tdef.actbox_name % info,
206 'url': tdef.actbox_url % info,
207 'permissions': (), # Predetermined.
208 'category': tdef.actbox_category,
209 'transition': tdef}))
210 res.sort()
211 return [ result[1] for result in res ]
213 security.declarePrivate('listGlobalActions')
214 def listGlobalActions(self, info):
215 '''
216 Allows this workflow to
217 include actions to be displayed in the actions box.
218 Called on every request.
219 Returns the actions to be displayed to the user.
220 '''
221 if not self.worklists:
222 return None # Optimization
223 sm = getSecurityManager()
224 portal = self._getPortalRoot()
225 res = []
226 fmt_data = None
227 for id, qdef in self.worklists.items():
228 if qdef.actbox_name:
229 guard = qdef.guard
230 if guard is None or guard.check(sm, self, portal):
231 searchres = None
232 var_match_keys = qdef.getVarMatchKeys()
233 if var_match_keys:
234 # Check the catalog for items in the worklist.
235 catalog = getToolByName(self, 'portal_catalog')
236 kw = {}
237 for k in var_match_keys:
238 v = qdef.getVarMatch(k)
239 kw[k] = [ x % info for x in v ]
240 searchres = catalog.searchResults(**kw)
241 if not searchres:
242 continue
243 if fmt_data is None:
244 fmt_data = TemplateDict()
245 fmt_data._push(info)
246 fmt_data._push({'count': len(searchres)})
247 res.append((id, {'id': id,
248 'name': qdef.actbox_name % fmt_data,
249 'url': qdef.actbox_url % fmt_data,
250 'permissions': (), # Predetermined.
251 'category': qdef.actbox_category}))
252 fmt_data._pop()
253 res.sort()
254 return [ result[1] for result in res ]
256 security.declarePrivate('isActionSupported')
257 def isActionSupported(self, ob, action, **kw):
258 '''
259 Returns a true value if the given action name
260 is possible in the current state.
261 '''
262 sdef = self._getWorkflowStateOf(ob)
263 if sdef is None:
264 return 0
265 if action in sdef.transitions:
266 tdef = self.transitions.get(action, None)
267 if (tdef is not None and
268 tdef.trigger_type == TRIGGER_USER_ACTION and
269 self._checkTransitionGuard(tdef, ob, **kw)):
270 return 1
271 return 0
273 security.declarePrivate('doActionFor')
274 def doActionFor(self, ob, action, comment='', **kw):
275 '''
276 Allows the user to request a workflow action. This method
277 must perform its own security checks.
278 '''
279 kw['comment'] = comment
280 sdef = self._getWorkflowStateOf(ob)
281 if sdef is None:
282 raise WorkflowException, 'Object is in an undefined state'
283 if action not in sdef.transitions:
284 raise Unauthorized(action)
285 tdef = self.transitions.get(action, None)
286 if tdef is None or tdef.trigger_type != TRIGGER_USER_ACTION:
287 raise WorkflowException, (
288 'Transition %s is not triggered by a user action' % action)
289 if not self._checkTransitionGuard(tdef, ob, **kw):
290 raise Unauthorized(action)
291 self._changeStateOf(ob, tdef, kw)
293 security.declarePrivate('isWorkflowMethodSupported')
294 def isWorkflowMethodSupported(self, ob, method_id):
295 '''
296 Returns a true value if the given workflow method
297 is supported in the current state.
298 '''
299 sdef = self._getWorkflowStateOf(ob)
300 if sdef is None:
301 return 0
302 if method_id in sdef.transitions:
303 tdef = self.transitions.get(method_id, None)
304 if (tdef is not None and
305 tdef.trigger_type == TRIGGER_WORKFLOW_METHOD and
306 self._checkTransitionGuard(tdef, ob)):
307 return 1
308 return 0
310 security.declarePrivate('wrapWorkflowMethod')
311 def wrapWorkflowMethod(self, ob, method_id, func, args, kw):
312 '''
313 Allows the user to request a workflow action. This method
314 must perform its own security checks.
315 '''
316 sdef = self._getWorkflowStateOf(ob)
317 if sdef is None:
318 raise WorkflowException, 'Object is in an undefined state'
319 if method_id not in sdef.transitions:
320 raise Unauthorized(method_id)
321 tdef = self.transitions.get(method_id, None)
322 if tdef is None or tdef.trigger_type != TRIGGER_WORKFLOW_METHOD:
323 raise WorkflowException, (
324 'Transition %s is not triggered by a workflow method'
325 % method_id)
326 if not self._checkTransitionGuard(tdef, ob):
327 raise Unauthorized(method_id)
328 res = func(*args, **kw)
329 try:
330 self._changeStateOf(ob, tdef)
331 except ObjectDeleted:
332 # Re-raise with a different result.
333 raise ObjectDeleted(res)
334 except ObjectMoved, ex:
335 # Re-raise with a different result.
336 raise ObjectMoved(ex.getNewObject(), res)
337 return res
339 security.declarePrivate('isInfoSupported')
340 def isInfoSupported(self, ob, name):
341 '''
342 Returns a true value if the given info name is supported.
343 '''
344 if name == self.state_var:
345 return 1
346 vdef = self.variables.get(name, None)
347 if vdef is None:
348 return 0
349 return 1
351 security.declarePrivate('getInfoFor')
352 def getInfoFor(self, ob, name, default):
353 '''
354 Allows the user to request information provided by the
355 workflow. This method must perform its own security checks.
356 '''
357 if name == self.state_var:
358 return self._getWorkflowStateOf(ob, 1)
359 vdef = self.variables[name]
360 if vdef.info_guard is not None and not vdef.info_guard.check(
361 getSecurityManager(), self, ob):
362 return default
363 status = self._getStatusOf(ob)
364 if status is not None and status.has_key(name):
365 value = status[name]
367 # Not set yet. Use a default.
368 elif vdef.default_expr is not None:
369 ec = createExprContext(StateChangeInfo(ob, self, status))
370 value = vdef.default_expr(ec)
371 else:
372 value = vdef.default_value
374 return value
376 security.declarePrivate('allowCreate')
377 def allowCreate(self, container, type_name):
378 """Returns true if the user is allowed to create a workflow instance.
380 The object passed to the guard is the prospective container.
381 """
382 if self.creation_guard is not None:
383 return self.creation_guard.check(
384 getSecurityManager(), self, container)
385 return 1
387 security.declarePrivate('notifyCreated')
388 def notifyCreated(self, ob):
389 """Notifies this workflow after an object has been created and added.
390 """
391 try:
392 self._changeStateOf(ob, None)
393 except ( ObjectDeleted, ObjectMoved ):
394 # Swallow.
395 pass
397 security.declarePrivate('notifyBefore')
398 def notifyBefore(self, ob, action):
399 '''
400 Notifies this workflow of an action before it happens,
401 allowing veto by exception. Unless an exception is thrown, either
402 a notifySuccess() or notifyException() can be expected later on.
403 The action usually corresponds to a method name.
404 '''
405 pass
407 security.declarePrivate('notifySuccess')
408 def notifySuccess(self, ob, action, result):
409 '''
410 Notifies this workflow that an action has taken place.
411 '''
412 pass
414 security.declarePrivate('notifyException')
415 def notifyException(self, ob, action, exc):
416 '''
417 Notifies this workflow that an action failed.
418 '''
419 pass
421 security.declarePrivate('updateRoleMappingsFor')
422 def updateRoleMappingsFor(self, ob):
423 """Changes the object permissions according to the current state.
424 """
425 changed = 0
426 sdef = self._getWorkflowStateOf(ob)
427 if sdef is None:
428 return 0
429 # Update the role -> permission map.
430 if self.permissions:
431 for p in self.permissions:
432 roles = []
433 if sdef.permission_roles is not None:
434 roles = sdef.permission_roles.get(p, roles)
435 if modifyRolesForPermission(ob, p, roles):
436 changed = 1
437 # Update the group -> role map.
438 groups = self.getGroups()
439 managed_roles = self.getRoles()
440 if groups and managed_roles:
441 for group in groups:
442 roles = ()
443 if sdef.group_roles is not None:
444 roles = sdef.group_roles.get(group, ())
445 if modifyRolesForGroup(ob, group, roles, managed_roles):
446 changed = 1
447 return changed
449 def _checkTransitionGuard(self, t, ob, **kw):
450 guard = t.guard
451 if guard is None:
452 return 1
453 if guard.check(getSecurityManager(), self, ob, **kw):
454 return 1
455 return 0
457 def _findAutomaticTransition(self, ob, sdef):
458 tdef = None
459 for tid in sdef.transitions:
460 t = self.transitions.get(tid, None)
461 if t is not None and t.trigger_type == TRIGGER_AUTOMATIC:
462 if self._checkTransitionGuard(t, ob):
463 tdef = t
464 break
465 return tdef
467 def _changeStateOf(self, ob, tdef=None, kwargs=None):
468 '''
469 Changes state. Can execute multiple transitions if there are
470 automatic transitions. tdef set to None means the object
471 was just created.
472 '''
473 moved_exc = None
474 while 1:
475 try:
476 sdef = self._executeTransition(ob, tdef, kwargs)
477 except ObjectMoved, moved_exc:
478 ob = moved_exc.getNewObject()
479 sdef = self._getWorkflowStateOf(ob)
480 # Re-raise after all transitions.
481 if sdef is None:
482 break
483 tdef = self._findAutomaticTransition(ob, sdef)
484 if tdef is None:
485 # No more automatic transitions.
486 break
487 # Else continue.
488 if moved_exc is not None:
489 # Re-raise.
490 raise moved_exc
492 def _executeTransition(self, ob, tdef=None, kwargs=None):
493 '''
494 Private method.
495 Puts object in a new state.
496 '''
497 sci = None
498 econtext = None
499 moved_exc = None
501 # Figure out the old and new states.
502 old_sdef = self._getWorkflowStateOf(ob)
503 old_state = old_sdef.getId()
504 if tdef is None:
505 new_state = self.initial_state
506 former_status = {}
507 else:
508 new_state = tdef.new_state_id
509 if not new_state:
510 # Stay in same state.
511 new_state = old_state
512 former_status = self._getStatusOf(ob)
513 new_sdef = self.states.get(new_state, None)
514 if new_sdef is None:
515 raise WorkflowException, (
516 'Destination state undefined: ' + new_state)
518 # Execute the "before" script.
519 if tdef is not None and tdef.script_name:
520 script = self.scripts[tdef.script_name]
521 # Pass lots of info to the script in a single parameter.
522 sci = StateChangeInfo(
523 ob, self, former_status, tdef, old_sdef, new_sdef, kwargs)
524 try:
525 script(sci) # May throw an exception.
526 except ObjectMoved, moved_exc:
527 ob = moved_exc.getNewObject()
528 # Re-raise after transition
530 # Update variables.
531 state_values = new_sdef.var_values
532 if state_values is None: state_values = {}
533 tdef_exprs = None
534 if tdef is not None: tdef_exprs = tdef.var_exprs
535 if tdef_exprs is None: tdef_exprs = {}
536 status = {}
537 for id, vdef in self.variables.items():
538 if not vdef.for_status:
539 continue
540 expr = None
541 if state_values.has_key(id):
542 value = state_values[id]
543 elif tdef_exprs.has_key(id):
544 expr = tdef_exprs[id]
545 elif not vdef.update_always and former_status.has_key(id):
546 # Preserve former value
547 value = former_status[id]
548 else:
549 if vdef.default_expr is not None:
550 expr = vdef.default_expr
551 else:
552 value = vdef.default_value
553 if expr is not None:
554 # Evaluate an expression.
555 if econtext is None:
556 # Lazily create the expression context.
557 if sci is None:
558 sci = StateChangeInfo(
559 ob, self, former_status, tdef,
560 old_sdef, new_sdef, kwargs)
561 econtext = createExprContext(sci)
562 value = expr(econtext)
563 status[id] = value
565 # Update state.
566 status[self.state_var] = new_state
567 tool = aq_parent(aq_inner(self))
568 tool.setStatusOf(self.id, ob, status)
570 # Update role to permission assignments.
571 self.updateRoleMappingsFor(ob)
573 # Execute the "after" script.
574 if tdef is not None and tdef.after_script_name:
575 script = self.scripts[tdef.after_script_name]
576 # Pass lots of info to the script in a single parameter.
577 sci = StateChangeInfo(
578 ob, self, status, tdef, old_sdef, new_sdef, kwargs)
579 script(sci) # May throw an exception.
581 # Return the new state object.
582 if moved_exc is not None:
583 # Propagate the notification that the object has moved.
584 raise moved_exc
585 else:
586 return new_sdef
588 InitializeClass(DCWorkflowDefinition)
591 addWorkflowFactory(DCWorkflowDefinition, id='dc_workflow',
592 title='Web-configurable workflow')