vendor/CMF/1.6.3/CMFCore

view WorkflowTool.py @ 2:4c712d7bd1d7

Added tag 1.6.3 for changeset 1babb9d61518
author Georges Racinet on purity.racinet.fr <georges@racinet.fr>
date Fri, 09 Sep 2011 12:44:00 +0200
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 """ Basic workflow tool.
15 $Id$
16 """
18 import sys
19 from warnings import warn
21 from AccessControl import ClassSecurityInfo
22 from Acquisition import aq_base, aq_inner, aq_parent
23 from Globals import DTMLFile
24 from Globals import InitializeClass
25 from Globals import PersistentMapping
26 from OFS.Folder import Folder
27 from OFS.ObjectManager import IFAwareObjectManager
29 from ActionProviderBase import ActionProviderBase
30 from interfaces import IWorkflowDefinition
31 from interfaces.portal_workflow import portal_workflow as IWorkflowTool
32 from permissions import ManagePortal
33 from utils import _dtmldir
34 from utils import getToolByName
35 from utils import UniqueObject
36 from WorkflowCore import ObjectDeleted
37 from WorkflowCore import ObjectMoved
38 from WorkflowCore import WorkflowException
41 _marker = [] # Create a new marker object.
43 class WorkflowInformation:
45 """ Shim implementation of ActionInformation, to enable
46 querying actions without mediation of the 'portal_actions' tool.
47 """
48 def __init__(self, object):
49 warn('WorkflowInformation() is deprecated and will be removed in '
50 'CMF 2.0.',
51 DeprecationWarning)
52 self.object = self.content = object
53 self.content_url = object.absolute_url()
54 self.portal_url = self.folder_url = ''
56 def __getitem__(self, name):
57 if name[:1] == '_':
58 raise KeyError, name
59 if hasattr(self, name):
60 return getattr(self, name)
61 raise KeyError, name
64 class WorkflowTool(UniqueObject, IFAwareObjectManager, Folder,
65 ActionProviderBase):
67 """ Mediator tool, mapping workflow objects
68 """
69 id = 'portal_workflow'
70 meta_type = 'CMF Workflow Tool'
71 __implements__ = (IWorkflowTool,
72 ActionProviderBase.__implements__)
73 _product_interfaces = (IWorkflowDefinition,)
75 _chains_by_type = None # PersistentMapping
76 _default_chain = ('default_workflow',)
77 _default_cataloging = 1
79 security = ClassSecurityInfo()
81 manage_options = ( { 'label' : 'Workflows'
82 , 'action' : 'manage_selectWorkflows'
83 }
84 , { 'label' : 'Overview', 'action' : 'manage_overview' }
85 ) + Folder.manage_options
87 #
88 # ZMI methods
89 #
90 security.declareProtected( ManagePortal, 'manage_overview' )
91 manage_overview = DTMLFile( 'explainWorkflowTool', _dtmldir )
93 _manage_addWorkflowForm = DTMLFile('addWorkflow', _dtmldir)
95 security.declareProtected( ManagePortal, 'manage_addWorkflowForm')
96 def manage_addWorkflowForm(self, REQUEST):
98 """ Form for adding workflows.
99 """
100 wft = []
101 for key in _workflow_factories.keys():
102 wft.append(key)
103 wft.sort()
104 return self._manage_addWorkflowForm(REQUEST, workflow_types=wft)
106 security.declareProtected( ManagePortal, 'manage_addWorkflow')
107 def manage_addWorkflow(self, workflow_type, id, RESPONSE=None):
109 """ Adds a workflow from the registered types.
110 """
111 factory = _workflow_factories[workflow_type]
112 ob = factory(id)
113 self._setObject(id, ob)
114 if RESPONSE is not None:
115 RESPONSE.redirect(self.absolute_url() +
116 '/manage_main?management_view=Contents')
118 _manage_selectWorkflows = DTMLFile('selectWorkflows', _dtmldir)
120 security.declareProtected( ManagePortal, 'manage_selectWorkflows')
121 def manage_selectWorkflows(self, REQUEST, manage_tabs_message=None):
123 """ Show a management screen for changing type to workflow connections.
124 """
125 cbt = self._chains_by_type
126 ti = self._listTypeInfo()
127 types_info = []
128 for t in ti:
129 id = t.getId()
130 title = t.Title()
131 if title == id:
132 title = None
133 if cbt is not None and cbt.has_key(id):
134 chain = ', '.join(cbt[id])
135 else:
136 chain = '(Default)'
137 types_info.append({'id': id,
138 'title': title,
139 'chain': chain})
140 return self._manage_selectWorkflows(
141 REQUEST,
142 default_chain=', '.join(self._default_chain),
143 types_info=types_info,
144 management_view='Workflows',
145 manage_tabs_message=manage_tabs_message)
147 security.declareProtected( ManagePortal, 'manage_changeWorkflows')
148 def manage_changeWorkflows(self, default_chain, props=None, REQUEST=None):
150 """ Changes which workflows apply to objects of which type.
151 """
152 if props is None:
153 props = REQUEST
154 cbt = self._chains_by_type
155 if cbt is None:
156 self._chains_by_type = cbt = PersistentMapping()
157 ti = self._listTypeInfo()
158 # Set up the chains by type.
159 if not (props is None):
160 for t in ti:
161 id = t.getId()
162 field_name = 'chain_%s' % id
163 chain = props.get(field_name, '(Default)').strip()
164 if chain == '(Default)':
165 # Remove from cbt.
166 if cbt.has_key(id):
167 del cbt[id]
168 else:
169 chain = chain.replace(',', ' ')
170 ids = []
171 for wf_id in chain.split(' '):
172 if wf_id:
173 if not self.getWorkflowById(wf_id):
174 raise ValueError, (
175 '"%s" is not a workflow ID.' % wf_id)
176 ids.append(wf_id)
177 cbt[id] = tuple(ids)
178 # Set up the default chain.
179 default_chain = default_chain.replace(',', ' ')
180 ids = []
181 for wf_id in default_chain.split(' '):
182 if wf_id:
183 if not self.getWorkflowById(wf_id):
184 raise ValueError, (
185 '"%s" is not a workflow ID.' % wf_id)
186 ids.append(wf_id)
187 self._default_chain = tuple(ids)
188 if REQUEST is not None:
189 return self.manage_selectWorkflows(REQUEST,
190 manage_tabs_message='Changed.')
192 #
193 # portal_workflow implementation.
194 #
195 security.declarePrivate('getCatalogVariablesFor')
196 def getCatalogVariablesFor(self, ob):
198 """ Returns a mapping of the catalog variables that apply to ob.
200 o Invoked by portal_catalog.
202 o Allows workflows to add variables to the catalog based on
203 workflow status, making it possible to implement queues.
204 """
205 wfs = self.getWorkflowsFor(ob)
206 if wfs is None:
207 return None
208 # Iterate through the workflows backwards so that
209 # earlier workflows can override later workflows.
210 wfs.reverse()
211 vars = {}
212 for wf in wfs:
213 v = wf.getCatalogVariablesFor(ob)
214 if v is not None:
215 vars.update(v)
216 return vars
218 security.declarePrivate('listActions')
219 def listActions(self, info=None, object=None):
221 """ Returns a list of actions to be displayed to the user.
223 o Invoked by the portal_actions tool.
225 o Allows workflows to include actions to be displayed in the
226 actions box.
228 o Object actions are supplied by workflows that apply to the object.
230 o Global actions are supplied by all workflows.
231 """
232 if object is not None or info is None:
233 info = self._getOAI(object)
234 chain = self.getChainFor(info.object)
235 did = {}
236 actions = []
238 for wf_id in chain:
239 did[wf_id] = 1
240 wf = self.getWorkflowById(wf_id)
241 if wf is not None:
242 a = wf.listObjectActions(info)
243 if a is not None:
244 actions.extend(a)
245 a = wf.listGlobalActions(info)
246 if a is not None:
247 actions.extend(a)
249 wf_ids = self.getWorkflowIds()
250 for wf_id in wf_ids:
251 if not did.has_key(wf_id):
252 wf = self.getWorkflowById(wf_id)
253 if wf is not None:
254 a = wf.listGlobalActions(info)
255 if a is not None:
256 actions.extend(a)
257 return actions
259 security.declarePublic('getActionsFor')
260 def getActionsFor(self, ob):
262 """ Return a list of action dictionaries for 'ob', just as though
263 queried via 'ActionsTool.listFilteredActionsFor'.
264 """
265 warn('getActionsFor() is deprecated and will be removed in CMF 2.0. '
266 'Please use listActionInfos() instead.',
267 DeprecationWarning)
268 return self.listActions( WorkflowInformation( ob ) )
270 security.declarePublic('doActionFor')
271 def doActionFor(self, ob, action, wf_id=None, *args, **kw):
273 """ Execute the given workflow action for the object.
275 o Invoked by user interface code.
277 o Allows the user to request a workflow action.
279 o The workflow object must perform its own security checks.
280 """
281 wfs = self.getWorkflowsFor(ob)
282 if wfs is None:
283 wfs = ()
284 if wf_id is None:
285 if not wfs:
286 raise WorkflowException('No workflows found.')
287 found = 0
288 for wf in wfs:
289 if wf.isActionSupported(ob, action, **kw):
290 found = 1
291 break
292 if not found:
293 raise WorkflowException(
294 'No workflow provides the "%s" action.' % action)
295 else:
296 wf = self.getWorkflowById(wf_id)
297 if wf is None:
298 raise WorkflowException(
299 'Requested workflow definition not found.')
300 return self._invokeWithNotification(
301 wfs, ob, action, wf.doActionFor, (ob, action) + args, kw)
303 security.declarePublic('getInfoFor')
304 def getInfoFor(self, ob, name, default=_marker, wf_id=None, *args, **kw):
306 """ Return a given workflow-specific property for an object.
308 o Invoked by user interface code.
310 o Allows the user to request information provided by the workflow.
312 o The workflow object must perform its own security checks.
313 """
314 if wf_id is None:
315 wfs = self.getWorkflowsFor(ob)
316 if wfs is None:
317 if default is _marker:
318 raise WorkflowException('No workflows found.')
319 else:
320 return default
321 found = 0
322 for wf in wfs:
323 if wf.isInfoSupported(ob, name):
324 found = 1
325 break
326 if not found:
327 if default is _marker:
328 raise WorkflowException(
329 'No workflow provides "%s" information.' % name)
330 else:
331 return default
332 else:
333 wf = self.getWorkflowById(wf_id)
334 if wf is None:
335 if default is _marker:
336 raise WorkflowException(
337 'Requested workflow definition not found.')
338 else:
339 return default
340 res = wf.getInfoFor(ob, name, default, *args, **kw)
341 if res is _marker:
342 raise WorkflowException('Could not get info: %s' % name)
343 return res
345 security.declarePrivate('notifyCreated')
346 def notifyCreated(self, ob):
348 """ Notify all applicable workflows that an object has been created
349 and put in its new place.
350 """
351 wfs = self.getWorkflowsFor(ob)
352 for wf in wfs:
353 wf.notifyCreated(ob)
354 self._reindexWorkflowVariables(ob)
356 security.declarePrivate('notifyBefore')
357 def notifyBefore(self, ob, action):
359 """ Notifies all applicable workflows of an action before it
360 happens, allowing veto by exception.
362 o Unless an exception is thrown, either a notifySuccess() or
363 notifyException() can be expected later on.
365 o The action usually corresponds to a method name.
366 """
367 wfs = self.getWorkflowsFor(ob)
368 for wf in wfs:
369 wf.notifyBefore(ob, action)
371 security.declarePrivate('notifySuccess')
372 def notifySuccess(self, ob, action, result=None):
374 """ Notify all applicable workflows that an action has taken place.
375 """
376 wfs = self.getWorkflowsFor(ob)
377 for wf in wfs:
378 wf.notifySuccess(ob, action, result)
380 security.declarePrivate('notifyException')
381 def notifyException(self, ob, action, exc):
383 """ Notify all applicable workflows that an action failed.
384 """
385 wfs = self.getWorkflowsFor(ob)
386 for wf in wfs:
387 wf.notifyException(ob, action, exc)
389 security.declarePrivate('getHistoryOf')
390 def getHistoryOf(self, wf_id, ob):
392 """ Return the history of an object.
394 o Invoked by workflow definitions.
395 """
396 if hasattr(aq_base(ob), 'workflow_history'):
397 wfh = ob.workflow_history
398 return wfh.get(wf_id, None)
399 return ()
401 security.declarePrivate('getStatusOf')
402 def getStatusOf(self, wf_id, ob):
404 """ Return the last entry of a workflow history.
406 o Invoked by workflow definitions.
407 """
408 wfh = self.getHistoryOf(wf_id, ob)
409 if wfh:
410 return wfh[-1]
411 return None
413 security.declarePrivate('setStatusOf')
414 def setStatusOf(self, wf_id, ob, status):
416 """ Append an entry to the workflow history.
418 o Invoked by workflow definitions.
419 """
420 wfh = None
421 has_history = 0
422 if hasattr(aq_base(ob), 'workflow_history'):
423 history = ob.workflow_history
424 if history is not None:
425 has_history = 1
426 wfh = history.get(wf_id, None)
427 if wfh is not None:
428 wfh = list(wfh)
429 if not wfh:
430 wfh = []
431 wfh.append(status)
432 if not has_history:
433 ob.workflow_history = PersistentMapping()
434 ob.workflow_history[wf_id] = tuple(wfh)
436 #
437 # Administration methods
438 #
439 security.declareProtected( ManagePortal, 'setDefaultChain')
440 def setDefaultChain(self, default_chain):
442 """ Set the default chain for this tool
443 """
444 default_chain = default_chain.replace(',', ' ')
445 ids = []
446 for wf_id in default_chain.split(' '):
447 if wf_id:
448 if not self.getWorkflowById(wf_id):
449 raise ValueError, ( '"%s" is not a workflow ID.' % wf_id)
450 ids.append(wf_id)
452 self._default_chain = tuple(ids)
454 security.declareProtected( ManagePortal, 'setChainForPortalTypes')
455 def setChainForPortalTypes(self, pt_names, chain, verify=True):
456 """ Set a chain for a specific portal type.
457 """
458 cbt = self._chains_by_type
459 if cbt is None:
460 self._chains_by_type = cbt = PersistentMapping()
462 if isinstance(chain, basestring):
463 chain = [ wf.strip() for wf in chain.split(',') if wf.strip() ]
465 ti_ids = [ t.getId() for t in self._listTypeInfo() ]
467 for type_id in pt_names:
468 if verify and not (type_id in ti_ids):
469 continue
470 cbt[type_id] = tuple(chain)
472 security.declareProtected( ManagePortal, 'updateRoleMappings')
473 def updateRoleMappings(self, REQUEST=None):
475 """ Allow workflows to update the role-permission mappings.
476 """
477 wfs = {}
478 for id in self.objectIds():
479 wf = self.getWorkflowById(id)
480 if hasattr(aq_base(wf), 'updateRoleMappingsFor'):
481 wfs[id] = wf
482 portal = aq_parent(aq_inner(self))
483 count = self._recursiveUpdateRoleMappings(portal, wfs)
484 if REQUEST is not None:
485 return self.manage_selectWorkflows(REQUEST, manage_tabs_message=
486 '%d object(s) updated.' % count)
487 else:
488 return count
490 security.declarePrivate('getWorkflowById')
491 def getWorkflowById(self, wf_id):
492 """ Retrieve a given workflow.
493 """
494 wf = getattr(self, wf_id, None)
495 if getattr(wf, '_isAWorkflow', False) or \
496 IWorkflowDefinition.providedBy(wf):
497 return wf
498 else:
499 return None
501 security.declarePrivate('getDefaultChainFor')
502 def getDefaultChainFor(self, ob):
504 """ Return the default chain, if applicable, for ob.
505 """
506 types_tool = getToolByName( self, 'portal_types', None )
507 if ( types_tool is not None
508 and types_tool.getTypeInfo( ob ) is not None ):
509 return self._default_chain
511 return ()
513 security.declarePrivate('getChainFor')
514 def getChainFor(self, ob):
516 """ Returns the chain that applies to the given object.
517 If we get a string as the ob parameter, use it as
518 the portal_type.
519 """
520 cbt = self._chains_by_type
521 if isinstance(ob, basestring):
522 pt = ob
523 elif hasattr(aq_base(ob), 'getPortalTypeName'):
524 pt = ob.getPortalTypeName()
525 else:
526 pt = None
528 if pt is None:
529 return ()
531 chain = None
532 if cbt is not None:
533 chain = cbt.get(pt, None)
534 # Note that if chain is not in cbt or has a value of
535 # None, we use a default chain.
536 if chain is None:
537 chain = self.getDefaultChainFor(ob)
538 if chain is None:
539 return ()
540 return chain
542 security.declarePrivate('getWorkflowIds')
543 def getWorkflowIds(self):
545 """ Return the list of workflow ids.
546 """
547 wf_ids = []
549 for obj_name, obj in self.objectItems():
550 if getattr(obj, '_isAWorkflow', 0):
551 wf_ids.append(obj_name)
553 return tuple(wf_ids)
555 security.declareProtected(ManagePortal, 'getWorkflowsFor')
556 def getWorkflowsFor(self, ob):
558 """ Find the workflows for the type of the given object.
559 """
560 res = []
561 for wf_id in self.getChainFor(ob):
562 wf = self.getWorkflowById(wf_id)
563 if wf is not None:
564 res.append(wf)
565 return res
567 security.declarePrivate('wrapWorkflowMethod')
568 def wrapWorkflowMethod(self, ob, method_id, func, args, kw):
570 """ To be invoked only by WorkflowCore.
571 Allows a workflow definition to wrap a WorkflowMethod.
572 """
573 wf = None
574 wfs = self.getWorkflowsFor(ob)
575 if wfs:
576 for w in wfs:
577 if (hasattr(w, 'isWorkflowMethodSupported')
578 and w.isWorkflowMethodSupported(ob, method_id)):
579 wf = w
580 break
581 else:
582 wfs = ()
583 if wf is None:
584 # No workflow wraps this method.
585 return func(*args, **kw)
586 return self._invokeWithNotification(
587 wfs, ob, method_id, wf.wrapWorkflowMethod,
588 (ob, method_id, func, args, kw), {})
590 #
591 # Helper methods
592 #
593 security.declarePrivate( '_listTypeInfo' )
594 def _listTypeInfo(self):
596 """ List the portal types which are available.
597 """
598 pt = getToolByName(self, 'portal_types', None)
599 if pt is None:
600 return ()
601 else:
602 return pt.listTypeInfo()
604 security.declarePrivate( '_invokeWithNotification' )
605 def _invokeWithNotification(self, wfs, ob, action, func, args, kw):
607 """ Private utility method: call 'func', and deal with exceptions
608 indicating that the object has been deleted or moved.
609 """
610 reindex = 1
611 for w in wfs:
612 w.notifyBefore(ob, action)
613 try:
614 res = func(*args, **kw)
615 except ObjectDeleted, ex:
616 res = ex.getResult()
617 reindex = 0
618 except ObjectMoved, ex:
619 res = ex.getResult()
620 ob = ex.getNewObject()
621 except:
622 exc = sys.exc_info()
623 try:
624 for w in wfs:
625 w.notifyException(ob, action, exc)
626 raise exc[0], exc[1], exc[2]
627 finally:
628 exc = None
629 for w in wfs:
630 w.notifySuccess(ob, action, res)
631 if reindex:
632 self._reindexWorkflowVariables(ob)
633 return res
635 security.declarePrivate( '_recursiveUpdateRoleMappings' )
636 def _recursiveUpdateRoleMappings(self, ob, wfs):
638 """ Update roles-permission mappings recursively, and
639 reindex special index.
640 """
641 # Returns a count of updated objects.
642 count = 0
643 wf_ids = self.getChainFor(ob)
644 if wf_ids:
645 changed = 0
646 for wf_id in wf_ids:
647 wf = wfs.get(wf_id, None)
648 if wf is not None:
649 did = wf.updateRoleMappingsFor(ob)
650 if did:
651 changed = 1
652 if changed:
653 count = count + 1
654 if hasattr(aq_base(ob), 'reindexObject'):
655 # Reindex security-related indexes
656 try:
657 ob.reindexObject(idxs=['allowedRolesAndUsers'])
658 except TypeError:
659 # Catch attempts to reindex portal_catalog.
660 pass
661 if hasattr(aq_base(ob), 'objectItems'):
662 obs = ob.objectItems()
663 if obs:
664 for k, v in obs:
665 changed = getattr(v, '_p_changed', 0)
666 count = count + self._recursiveUpdateRoleMappings(v, wfs)
667 if changed is None:
668 # Re-ghostify.
669 v._p_deactivate()
670 return count
672 security.declarePrivate( '_setDefaultCataloging' )
673 def _setDefaultCataloging( self, value ):
675 """ Toggle whether '_reindexWorkflowVariables' actually touches
676 the catalog (sometimes not desirable, e.g. when the workflow
677 objects do this themselves only at particular points).
678 """
679 self._default_cataloging = bool(value)
681 security.declarePrivate('_reindexWorkflowVariables')
682 def _reindexWorkflowVariables(self, ob):
684 """ Reindex the variables that the workflow may have changed.
686 Also reindexes the security.
687 """
688 if not self._default_cataloging:
689 return
691 if hasattr(aq_base(ob), 'reindexObject'):
692 # XXX We only need the keys here, no need to compute values.
693 mapping = self.getCatalogVariablesFor(ob) or {}
694 vars = mapping.keys()
695 ob.reindexObject(idxs=vars)
697 # Reindex security of subobjects.
698 if hasattr(aq_base(ob), 'reindexObjectSecurity'):
699 ob.reindexObjectSecurity()
701 InitializeClass(WorkflowTool)
704 _workflow_factories = {}
706 def _makeWorkflowFactoryKey(factory, id=None, title=None):
707 # The factory should take one argument, id.
708 if id is None:
709 id = getattr(factory, 'id', '') or getattr(factory, 'meta_type', '')
710 if title is None:
711 title = getattr(factory, 'title', '')
712 key = id
713 if title:
714 key = key + ' (%s)' % title
715 return key
717 def addWorkflowFactory(factory, id=None, title=None):
718 key = _makeWorkflowFactoryKey( factory, id, title )
719 _workflow_factories[key] = factory
721 addWorkflowClass = addWorkflowFactory # bw compat.
724 def _removeWorkflowFactory( factory, id=None, title=None ):
725 """ Make teardown in unitcase cleaner. """
726 key = _makeWorkflowFactoryKey( factory, id, title )
727 try:
728 del _workflow_factories[key]
729 except KeyError:
730 pass