Back to index

plone3  3.1.7
WorkflowTool.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
00004 #
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE.
00011 #
00012 ##############################################################################
00013 """ Basic workflow tool.
00014 
00015 $Id: WorkflowTool.py 77182 2007-06-28 17:25:27Z yuppie $
00016 """
00017 
00018 import sys
00019 from warnings import warn
00020 
00021 from AccessControl import ClassSecurityInfo
00022 from AccessControl.requestmethod import postonly
00023 from Acquisition import aq_base, aq_inner, aq_parent
00024 from Globals import DTMLFile
00025 from Globals import InitializeClass
00026 from Globals import PersistentMapping
00027 from OFS.Folder import Folder
00028 from OFS.ObjectManager import IFAwareObjectManager
00029 from zope.event import notify
00030 from zope.interface import implements
00031 
00032 from ActionProviderBase import ActionProviderBase
00033 from interfaces import IConfigurableWorkflowTool
00034 from interfaces import IWorkflowDefinition
00035 from interfaces import IWorkflowTool
00036 from interfaces.portal_workflow import portal_workflow as z2IWorkflowTool
00037 from permissions import ManagePortal
00038 from utils import _dtmldir
00039 from utils import getToolByName
00040 from utils import Message as _
00041 from utils import UniqueObject
00042 from WorkflowCore import ActionRaisedExceptionEvent
00043 from WorkflowCore import ActionSucceededEvent
00044 from WorkflowCore import ActionWillBeInvokedEvent
00045 from WorkflowCore import ObjectDeleted
00046 from WorkflowCore import ObjectMoved
00047 from WorkflowCore import WorkflowException
00048 
00049 _marker = []  # Create a new marker object.
00050 
00051 
00052 class WorkflowTool(UniqueObject, IFAwareObjectManager, Folder,
00053                    ActionProviderBase):
00054 
00055     """ Mediator tool, mapping workflow objects
00056     """
00057 
00058     implements(IConfigurableWorkflowTool, IWorkflowTool)
00059     __implements__ = (z2IWorkflowTool, ActionProviderBase.__implements__)
00060 
00061     id = 'portal_workflow'
00062     meta_type = 'CMF Workflow Tool'
00063     _product_interfaces = (IWorkflowDefinition,)
00064 
00065     _chains_by_type = None  # PersistentMapping
00066     _default_chain = ('default_workflow',)
00067     _default_cataloging = 1
00068 
00069     security = ClassSecurityInfo()
00070 
00071     manage_options = ( { 'label' : 'Workflows'
00072                        , 'action' : 'manage_selectWorkflows'
00073                        }
00074                      , { 'label' : 'Overview', 'action' : 'manage_overview' }
00075                      ) + Folder.manage_options
00076 
00077     #
00078     #   ZMI methods
00079     #
00080     security.declareProtected( ManagePortal, 'manage_overview' )
00081     manage_overview = DTMLFile( 'explainWorkflowTool', _dtmldir )
00082 
00083     _manage_selectWorkflows = DTMLFile('selectWorkflows', _dtmldir)
00084 
00085     security.declareProtected( ManagePortal, 'manage_selectWorkflows')
00086     def manage_selectWorkflows(self, REQUEST, manage_tabs_message=None):
00087 
00088         """ Show a management screen for changing type to workflow connections.
00089         """
00090         cbt = self._chains_by_type
00091         ti = self._listTypeInfo()
00092         types_info = []
00093         for t in ti:
00094             id = t.getId()
00095             title = t.Title()
00096             if title == id:
00097                 title = None
00098             if cbt is not None and cbt.has_key(id):
00099                 chain = ', '.join(cbt[id])
00100             else:
00101                 chain = '(Default)'
00102             types_info.append({'id': id,
00103                                'title': title,
00104                                'chain': chain})
00105         return self._manage_selectWorkflows(
00106             REQUEST,
00107             default_chain=', '.join(self._default_chain),
00108             types_info=types_info,
00109             management_view='Workflows',
00110             manage_tabs_message=manage_tabs_message)
00111 
00112     security.declareProtected( ManagePortal, 'manage_changeWorkflows')
00113     @postonly
00114     def manage_changeWorkflows(self, default_chain, props=None, REQUEST=None):
00115         """ Changes which workflows apply to objects of which type.
00116         """
00117         if props is None:
00118             props = REQUEST
00119         cbt = self._chains_by_type
00120         if cbt is None:
00121             self._chains_by_type = cbt = PersistentMapping()
00122         ti = self._listTypeInfo()
00123         # Set up the chains by type.
00124         if not (props is None):
00125             for t in ti:
00126                 id = t.getId()
00127                 field_name = 'chain_%s' % id
00128                 chain = props.get(field_name, '(Default)').strip()
00129                 if chain == '(Default)':
00130                     # Remove from cbt.
00131                     if cbt.has_key(id):
00132                         del cbt[id]
00133                 else:
00134                     chain = chain.replace(',', ' ')
00135                     ids = []
00136                     for wf_id in chain.split(' '):
00137                         if wf_id:
00138                             if not self.getWorkflowById(wf_id):
00139                                 raise ValueError, (
00140                                     '"%s" is not a workflow ID.' % wf_id)
00141                             ids.append(wf_id)
00142                     cbt[id] = tuple(ids)
00143         # Set up the default chain.
00144         default_chain = default_chain.replace(',', ' ')
00145         ids = []
00146         for wf_id in default_chain.split(' '):
00147             if wf_id:
00148                 if not self.getWorkflowById(wf_id):
00149                     raise ValueError, (
00150                         '"%s" is not a workflow ID.' % wf_id)
00151                 ids.append(wf_id)
00152         self._default_chain = tuple(ids)
00153         if REQUEST is not None:
00154             return self.manage_selectWorkflows(REQUEST,
00155                             manage_tabs_message='Changed.')
00156 
00157     #
00158     #   'IActionProvider' interface methods
00159     #
00160     security.declarePrivate('listActions')
00161     def listActions(self, info=None, object=None):
00162 
00163         """ Returns a list of actions to be displayed to the user.
00164 
00165         o Invoked by the portal_actions tool.
00166 
00167         o Allows workflows to include actions to be displayed in the
00168           actions box.
00169 
00170         o Object actions are supplied by workflows that apply to the object.
00171 
00172         o Global actions are supplied by all workflows.
00173         """
00174         if object is not None or info is None:
00175             info = self._getOAI(object)
00176         chain = self.getChainFor(info.object)
00177         did = {}
00178         actions = []
00179 
00180         for wf_id in chain:
00181             did[wf_id] = 1
00182             wf = self.getWorkflowById(wf_id)
00183             if wf is not None:
00184                 a = wf.listObjectActions(info)
00185                 if a is not None:
00186                     actions.extend(a)
00187                 a = wf.listGlobalActions(info)
00188                 if a is not None:
00189                     actions.extend(a)
00190 
00191         wf_ids = self.getWorkflowIds()
00192         for wf_id in wf_ids:
00193             if not did.has_key(wf_id):
00194                 wf = self.getWorkflowById(wf_id)
00195                 if wf is not None:
00196                     a = wf.listGlobalActions(info)
00197                     if a is not None:
00198                         actions.extend(a)
00199         return actions
00200 
00201     #
00202     #   'IWorkflowTool' interface methods
00203     #
00204     security.declarePrivate('getCatalogVariablesFor')
00205     def getCatalogVariablesFor(self, ob):
00206         """ Get a mapping of "workflow-relevant" attributes.
00207         """
00208         wfs = self.getWorkflowsFor(ob)
00209         if wfs is None:
00210             return None
00211         # Iterate through the workflows backwards so that
00212         # earlier workflows can override later workflows.
00213         wfs.reverse()
00214         vars = {}
00215         for wf in wfs:
00216             v = wf.getCatalogVariablesFor(ob)
00217             if v is not None:
00218                 vars.update(v)
00219         return vars
00220 
00221     security.declarePublic('doActionFor')
00222     def doActionFor(self, ob, action, wf_id=None, *args, **kw):
00223         """ Perform the given workflow action on 'ob'.
00224         """
00225         wfs = self.getWorkflowsFor(ob)
00226         if wfs is None:
00227             wfs = ()
00228         if wf_id is None:
00229             if not wfs:
00230                 raise WorkflowException(_(u'No workflows found.'))
00231             found = 0
00232             for wf in wfs:
00233                 if wf.isActionSupported(ob, action, **kw):
00234                     found = 1
00235                     break
00236             if not found:
00237                 msg = _(u"No workflow provides the '${action_id}' action.",
00238                         mapping={'action_id': action})
00239                 raise WorkflowException(msg)
00240         else:
00241             wf = self.getWorkflowById(wf_id)
00242             if wf is None:
00243                 raise WorkflowException(
00244                     _(u'Requested workflow definition not found.'))
00245         return self._invokeWithNotification(
00246             wfs, ob, action, wf.doActionFor, (ob, action) + args, kw)
00247 
00248     security.declarePublic('getInfoFor')
00249     def getInfoFor(self, ob, name, default=_marker, wf_id=None, *args, **kw):
00250         """ Get the given bit of workflow information for the object.
00251         """
00252         if wf_id is None:
00253             wfs = self.getWorkflowsFor(ob)
00254             if wfs is None:
00255                 if default is _marker:
00256                     raise WorkflowException(_(u'No workflows found.'))
00257                 else:
00258                     return default
00259             found = 0
00260             for wf in wfs:
00261                 if wf.isInfoSupported(ob, name):
00262                     found = 1
00263                     break
00264             if not found:
00265                 if default is _marker:
00266                     msg = _(u"No workflow provides '${name}' information.",
00267                             mapping={'name': name})
00268                     raise WorkflowException(msg)
00269                 else:
00270                     return default
00271         else:
00272             wf = self.getWorkflowById(wf_id)
00273             if wf is None:
00274                 if default is _marker:
00275                     raise WorkflowException(
00276                         _(u'Requested workflow definition not found.'))
00277                 else:
00278                     return default
00279         res = wf.getInfoFor(ob, name, default, *args, **kw)
00280         if res is _marker:
00281             msg = _(u'Could not get info: ${name}', mapping={'name': name})
00282             raise WorkflowException(msg)
00283         return res
00284 
00285     security.declarePrivate('notifyCreated')
00286     def notifyCreated(self, ob):
00287         """ Notify all applicable workflows that an object has been created.
00288         """
00289         wfs = self.getWorkflowsFor(ob)
00290         for wf in wfs:
00291             wf.notifyCreated(ob)
00292         self._reindexWorkflowVariables(ob)
00293 
00294     security.declarePrivate('notifyBefore')
00295     def notifyBefore(self, ob, action):
00296         """ Notify all applicable workflows of an action before it happens.
00297         """
00298         wfs = self.getWorkflowsFor(ob)
00299         for wf in wfs:
00300             wf.notifyBefore(ob, action)
00301             notify(ActionWillBeInvokedEvent(ob, wf, action))
00302 
00303     security.declarePrivate('notifySuccess')
00304     def notifySuccess(self, ob, action, result=None):
00305         """ Notify all applicable workflows that an action has taken place.
00306         """
00307         wfs = self.getWorkflowsFor(ob)
00308         for wf in wfs:
00309             wf.notifySuccess(ob, action, result)
00310             notify(ActionSucceededEvent(ob, wf, action, result))
00311 
00312     security.declarePrivate('notifyException')
00313     def notifyException(self, ob, action, exc):
00314         """ Notify all applicable workflows that an action failed.
00315         """
00316         wfs = self.getWorkflowsFor(ob)
00317         for wf in wfs:
00318             wf.notifyException(ob, action, exc)
00319             notify(ActionRaisedExceptionEvent(ob, wf, action, exc))
00320 
00321     security.declarePrivate('getHistoryOf')
00322     def getHistoryOf(self, wf_id, ob):
00323         """ Get the history of an object for a given workflow.
00324         """
00325         if hasattr(aq_base(ob), 'workflow_history'):
00326             wfh = ob.workflow_history
00327             return wfh.get(wf_id, None)
00328         return ()
00329 
00330     security.declarePrivate('getStatusOf')
00331     def getStatusOf(self, wf_id, ob):
00332         """ Get the last element of a workflow history for a given workflow.
00333         """
00334         wfh = self.getHistoryOf(wf_id, ob)
00335         if wfh:
00336             return wfh[-1]
00337         return None
00338 
00339     security.declarePrivate('setStatusOf')
00340     def setStatusOf(self, wf_id, ob, status):
00341         """ Append a record to the workflow history of a given workflow.
00342         """
00343         wfh = None
00344         has_history = 0
00345         if hasattr(aq_base(ob), 'workflow_history'):
00346             history = ob.workflow_history
00347             if history is not None:
00348                 has_history = 1
00349                 wfh = history.get(wf_id, None)
00350                 if wfh is not None:
00351                     wfh = list(wfh)
00352         if not wfh:
00353             wfh = []
00354         wfh.append(status)
00355         if not has_history:
00356             ob.workflow_history = PersistentMapping()
00357         ob.workflow_history[wf_id] = tuple(wfh)
00358 
00359     #
00360     #   'IConfigurableWorkflowTool' interface methods
00361     #
00362     security.declareProtected(ManagePortal, 'setDefaultChain')
00363     @postonly
00364     def setDefaultChain(self, default_chain, REQUEST=None):
00365         """ Set the default chain for this tool.
00366         """
00367         default_chain = default_chain.replace(',', ' ')
00368         ids = []
00369         for wf_id in default_chain.split(' '):
00370             if wf_id:
00371                 if not self.getWorkflowById(wf_id):
00372                     raise ValueError('"%s" is not a workflow ID.' % wf_id)
00373                 ids.append(wf_id)
00374 
00375         self._default_chain = tuple(ids)
00376 
00377     security.declareProtected(ManagePortal, 'setChainForPortalTypes')
00378     @postonly
00379     def setChainForPortalTypes(self, pt_names, chain, verify=True,
00380                                REQUEST=None):
00381         """ Set a chain for specific portal types.
00382         """
00383         cbt = self._chains_by_type
00384         if cbt is None:
00385             self._chains_by_type = cbt = PersistentMapping()
00386 
00387         if chain is None:
00388             for type_id in pt_names:
00389                 if cbt.has_key(type_id):
00390                     del cbt[type_id]
00391             return
00392 
00393         if isinstance(chain, basestring):
00394             if chain == '(Default)':
00395                 chain = ''
00396             else:
00397                 chain = [ wf.strip() for wf in chain.split(',') if wf.strip() ]
00398 
00399         ti_ids = [ t.getId() for t in self._listTypeInfo() ]
00400 
00401         for type_id in pt_names:
00402             if verify and not (type_id in ti_ids):
00403                 continue
00404             cbt[type_id] = tuple(chain)
00405 
00406     security.declarePrivate('getDefaultChain')
00407     def getDefaultChain(self):
00408         """ Get the default chain for this tool.
00409         """
00410         return self._default_chain
00411 
00412     security.declarePrivate('listChainOverrides')
00413     def listChainOverrides(self):
00414         """ List portal type specific chain overrides.
00415         """
00416         cbt = self._chains_by_type
00417         return cbt and sorted(cbt.items()) or ()
00418 
00419     security.declarePrivate('getChainFor')
00420     def getChainFor(self, ob):
00421         """ Get the chain that applies to the given object.
00422         """
00423         cbt = self._chains_by_type
00424         if isinstance(ob, basestring):
00425             pt = ob
00426         elif hasattr(aq_base(ob), 'getPortalTypeName'):
00427             pt = ob.getPortalTypeName()
00428         else:
00429             pt = None
00430 
00431         if pt is None:
00432             return ()
00433 
00434         chain = None
00435         if cbt is not None:
00436             chain = cbt.get(pt, None)
00437             # Note that if chain is not in cbt or has a value of
00438             # None, we use a default chain.
00439         if chain is None:
00440             return self.getDefaultChain()
00441         return chain
00442 
00443     #
00444     #   Other methods
00445     #
00446     security.declareProtected(ManagePortal, 'updateRoleMappings')
00447     @postonly
00448     def updateRoleMappings(self, REQUEST=None):
00449         """ Allow workflows to update the role-permission mappings.
00450         """
00451         wfs = {}
00452         for id in self.objectIds():
00453             wf = self.getWorkflowById(id)
00454             if hasattr(aq_base(wf), 'updateRoleMappingsFor'):
00455                 wfs[id] = wf
00456         portal = aq_parent(aq_inner(self))
00457         count = self._recursiveUpdateRoleMappings(portal, wfs)
00458         if REQUEST is not None:
00459             return self.manage_selectWorkflows(REQUEST, manage_tabs_message=
00460                                                '%d object(s) updated.' % count)
00461         else:
00462             return count
00463 
00464     security.declarePrivate('getWorkflowById')
00465     def getWorkflowById(self, wf_id):
00466         """ Retrieve a given workflow.
00467         """
00468         wf = getattr(self, wf_id, None)
00469         if IWorkflowDefinition.providedBy(wf):
00470             return wf
00471         if getattr(wf, '_isAWorkflow', False):
00472             # BBB
00473             warn("The '_isAWorkflow' marker attribute for workflow "
00474                  "definitions is deprecated and will be removed in "
00475                  "CMF 2.3;  please mark the definition with "
00476                  "'IWorkflowDefinition' instead.",
00477                  DeprecationWarning, stacklevel=2)
00478             return wf
00479         else:
00480             return None
00481 
00482     security.declarePrivate('getDefaultChainFor')
00483     def getDefaultChainFor(self, ob):
00484         """ Get the default chain, if applicable, for ob.
00485         """
00486         # XXX: this method violates the rules for tools/utilities:
00487         # it depends on a non-utility tool
00488         types_tool = getToolByName( self, 'portal_types', None )
00489         if ( types_tool is not None
00490             and types_tool.getTypeInfo( ob ) is not None ):
00491             return self._default_chain
00492 
00493         return ()
00494 
00495     security.declarePrivate('getWorkflowIds')
00496     def getWorkflowIds(self):
00497 
00498         """ Return the list of workflow ids.
00499         """
00500         wf_ids = []
00501 
00502         for obj_name, obj in self.objectItems():
00503             if IWorkflowDefinition.providedBy(obj):
00504                 wf_ids.append(obj_name)
00505             elif getattr(obj, '_isAWorkflow', 0):
00506                 # BBB
00507                 warn("The '_isAWorkflow' marker attribute for workflow "
00508                      "definitions is deprecated and will be removed in "
00509                      "CMF 2.3;  please mark the definition with "
00510                      "'IWorkflowDefinition' instead.",
00511                      DeprecationWarning, stacklevel=2)
00512                 wf_ids.append(obj_name)
00513 
00514         return tuple(wf_ids)
00515 
00516     security.declareProtected(ManagePortal, 'getWorkflowsFor')
00517     def getWorkflowsFor(self, ob):
00518 
00519         """ Find the workflows for the type of the given object.
00520         """
00521         res = []
00522         for wf_id in self.getChainFor(ob):
00523             wf = self.getWorkflowById(wf_id)
00524             if wf is not None:
00525                 res.append(wf)
00526         return res
00527 
00528     #
00529     #   Helper methods
00530     #
00531     security.declarePrivate( '_listTypeInfo' )
00532     def _listTypeInfo(self):
00533 
00534         """ List the portal types which are available.
00535         """
00536         # XXX: this method violates the rules for tools/utilities:
00537         # it depends on a non-utility tool
00538         pt = getToolByName(self, 'portal_types', None)
00539         if pt is None:
00540             return ()
00541         else:
00542             return pt.listTypeInfo()
00543 
00544     security.declarePrivate( '_invokeWithNotification' )
00545     def _invokeWithNotification(self, wfs, ob, action, func, args, kw):
00546 
00547         """ Private utility method:  call 'func', and deal with exceptions
00548             indicating that the object has been deleted or moved.
00549         """
00550         reindex = 1
00551         for w in wfs:
00552             w.notifyBefore(ob, action)
00553             notify(ActionWillBeInvokedEvent(ob, w, action))
00554         try:
00555             res = func(*args, **kw)
00556         except ObjectDeleted, ex:
00557             res = ex.getResult()
00558             reindex = 0
00559         except ObjectMoved, ex:
00560             res = ex.getResult()
00561             ob = ex.getNewObject()
00562         except:
00563             exc = sys.exc_info()
00564             try:
00565                 for w in wfs:
00566                     w.notifyException(ob, action, exc)
00567                     notify(ActionRaisedExceptionEvent(ob, w, action, exc))
00568                 raise exc[0], exc[1], exc[2]
00569             finally:
00570                 exc = None
00571         for w in wfs:
00572             w.notifySuccess(ob, action, res)
00573             notify(ActionSucceededEvent(ob, w, action, res))
00574         if reindex:
00575             self._reindexWorkflowVariables(ob)
00576         return res
00577 
00578     security.declarePrivate( '_recursiveUpdateRoleMappings' )
00579     def _recursiveUpdateRoleMappings(self, ob, wfs):
00580 
00581         """ Update roles-permission mappings recursively, and
00582             reindex special index.
00583         """
00584         # Returns a count of updated objects.
00585         count = 0
00586         wf_ids = self.getChainFor(ob)
00587         if wf_ids:
00588             changed = 0
00589             for wf_id in wf_ids:
00590                 wf = wfs.get(wf_id, None)
00591                 if wf is not None:
00592                     did = wf.updateRoleMappingsFor(ob)
00593                     if did:
00594                         changed = 1
00595             if changed:
00596                 count = count + 1
00597                 if hasattr(aq_base(ob), 'reindexObject'):
00598                     # Reindex security-related indexes
00599                     try:
00600                         ob.reindexObject(idxs=['allowedRolesAndUsers'])
00601                     except TypeError:
00602                         # Catch attempts to reindex portal_catalog.
00603                         pass
00604         if hasattr(aq_base(ob), 'objectItems'):
00605             obs = ob.objectItems()
00606             if obs:
00607                 for k, v in obs:
00608                     changed = getattr(v, '_p_changed', 0)
00609                     count = count + self._recursiveUpdateRoleMappings(v, wfs)
00610                     if changed is None:
00611                         # Re-ghostify.
00612                         v._p_deactivate()
00613         return count
00614 
00615     security.declarePrivate( '_setDefaultCataloging' )
00616     def _setDefaultCataloging( self, value ):
00617 
00618         """ Toggle whether '_reindexWorkflowVariables' actually touches
00619             the catalog (sometimes not desirable, e.g. when the workflow
00620             objects do this themselves only at particular points).
00621         """
00622         self._default_cataloging = bool(value)
00623 
00624     security.declarePrivate('_reindexWorkflowVariables')
00625     def _reindexWorkflowVariables(self, ob):
00626 
00627         """ Reindex the variables that the workflow may have changed.
00628 
00629         Also reindexes the security.
00630         """
00631         if not self._default_cataloging:
00632             return
00633 
00634         if hasattr(aq_base(ob), 'reindexObject'):
00635             # XXX We only need the keys here, no need to compute values.
00636             mapping = self.getCatalogVariablesFor(ob) or {}
00637             vars = mapping.keys()
00638             ob.reindexObject(idxs=vars)
00639 
00640         # Reindex security of subobjects.
00641         if hasattr(aq_base(ob), 'reindexObjectSecurity'):
00642             ob.reindexObjectSecurity()
00643 
00644 InitializeClass(WorkflowTool)