Back to index

plone3  3.1.7
DCWorkflow.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 """ Web-configurable workflow.
00014 
00015 $Id: DCWorkflow.py 78745 2007-08-10 20:19:46Z mj $
00016 """
00017 
00018 # Zope
00019 from AccessControl import ClassSecurityInfo
00020 from AccessControl import getSecurityManager
00021 from AccessControl import Unauthorized
00022 from Acquisition import aq_inner
00023 from Acquisition import aq_parent
00024 from DocumentTemplate.DT_Util import TemplateDict
00025 from Globals import InitializeClass
00026 from OFS.Folder import Folder
00027 from OFS.ObjectManager import bad_id
00028 from zope.event import notify
00029 from zope.interface import implements
00030 
00031 # CMFCore
00032 from Products.CMFCore.interfaces import IWorkflowDefinition
00033 from Products.CMFCore.interfaces.portal_workflow \
00034         import WorkflowDefinition as z2IWorkflowDefinition
00035 from Products.CMFCore.utils import getToolByName
00036 from Products.CMFCore.WorkflowCore import ObjectDeleted
00037 from Products.CMFCore.WorkflowCore import ObjectMoved
00038 from Products.CMFCore.WorkflowCore import WorkflowException
00039 
00040 # DCWorkflow
00041 from events import BeforeTransitionEvent, AfterTransitionEvent
00042 from Expression import createExprContext
00043 from Expression import StateChangeInfo
00044 from interfaces import IDCWorkflowDefinition
00045 from permissions import ManagePortal
00046 from Transitions import TRIGGER_AUTOMATIC
00047 from Transitions import TRIGGER_USER_ACTION
00048 from utils import Message as _
00049 from utils import modifyRolesForGroup
00050 from utils import modifyRolesForPermission
00051 from WorkflowUIMixin import WorkflowUIMixin
00052 
00053 def checkId(id):
00054     res = bad_id(id)
00055     if res != -1 and res is not None:
00056         raise ValueError, 'Illegal ID'
00057     return 1
00058 
00059 
00060 class DCWorkflowDefinition(WorkflowUIMixin, Folder):
00061 
00062     '''
00063     This class is the workflow engine and the container for the
00064     workflow definition.
00065     UI methods are in WorkflowUIMixin.
00066     '''
00067 
00068     implements(IDCWorkflowDefinition, IWorkflowDefinition)
00069     __implements__ = z2IWorkflowDefinition
00070 
00071     title = 'DC Workflow Definition'
00072     description = ''
00073 
00074     state_var = 'state'
00075     initial_state = None
00076 
00077     states = None
00078     transitions = None
00079     variables = None
00080     worklists = None
00081     scripts = None
00082 
00083     permissions = ()
00084     groups = ()     # Names of groups managed by this workflow.
00085     roles = None  # The role names managed by this workflow.
00086     # If roles is None, listRoles() provides a default.
00087 
00088     creation_guard = None  # The guard that can veto object creation.
00089 
00090     manager_bypass = 0  # Boolean: 'Manager' role bypasses guards
00091 
00092     manage_options = (
00093         {'label': 'Properties', 'action': 'manage_properties'},
00094         {'label': 'States', 'action': 'states/manage_main'},
00095         {'label': 'Transitions', 'action': 'transitions/manage_main'},
00096         {'label': 'Variables', 'action': 'variables/manage_main'},
00097         {'label': 'Worklists', 'action': 'worklists/manage_main'},
00098         {'label': 'Scripts', 'action': 'scripts/manage_main'},
00099         {'label': 'Permissions', 'action': 'manage_permissions'},
00100         {'label': 'Groups', 'action': 'manage_groups'},
00101         )
00102 
00103     security = ClassSecurityInfo()
00104     security.declareObjectProtected(ManagePortal)
00105 
00106     def __init__(self, id):
00107         self.id = id
00108         from States import States
00109         self._addObject(States('states'))
00110         from Transitions import Transitions
00111         self._addObject(Transitions('transitions'))
00112         from Variables import Variables
00113         self._addObject(Variables('variables'))
00114         from Worklists import Worklists
00115         self._addObject(Worklists('worklists'))
00116         from Scripts import Scripts
00117         self._addObject(Scripts('scripts'))
00118 
00119     def _addObject(self, ob):
00120         id = ob.getId()
00121         setattr(self, id, ob)
00122         self._objects = self._objects + (
00123             {'id': id, 'meta_type': ob.meta_type},)
00124 
00125     #
00126     # Workflow engine.
00127     #
00128 
00129     def _getStatusOf(self, ob):
00130         tool = aq_parent(aq_inner(self))
00131         status = tool.getStatusOf(self.id, ob)
00132         if status is None:
00133             return {}
00134         else:
00135             return status
00136 
00137     def _getWorkflowStateOf(self, ob, id_only=0):
00138         tool = aq_parent(aq_inner(self))
00139         status = tool.getStatusOf(self.id, ob)
00140         if status is None:
00141             state = self.initial_state
00142         else:
00143             state = status.get(self.state_var, None)
00144             if state is None:
00145                 state = self.initial_state
00146         if id_only:
00147             return state
00148         else:
00149             return self.states.get(state, None)
00150 
00151     def _getPortalRoot(self):
00152         return aq_parent(aq_inner(aq_parent(aq_inner(self))))
00153 
00154     security.declarePrivate('getCatalogVariablesFor')
00155     def getCatalogVariablesFor(self, ob):
00156         '''
00157         Allows this workflow to make workflow-specific variables
00158         available to the catalog, making it possible to implement
00159         worklists in a simple way.
00160         Returns a mapping containing the catalog variables
00161         that apply to ob.
00162         '''
00163         res = {}
00164         status = self._getStatusOf(ob)
00165         for id, vdef in self.variables.items():
00166             if vdef.for_catalog:
00167                 if status.has_key(id):
00168                     value = status[id]
00169 
00170                 # Not set yet.  Use a default.
00171                 elif vdef.default_expr is not None:
00172                     ec = createExprContext(StateChangeInfo(ob, self, status))
00173                     value = vdef.default_expr(ec)
00174                 else:
00175                     value = vdef.default_value
00176 
00177                 res[id] = value
00178         # Always provide the state variable.
00179         state_var = self.state_var
00180         res[state_var] = status.get(state_var, self.initial_state)
00181         return res
00182 
00183     security.declarePrivate('listObjectActions')
00184     def listObjectActions(self, info):
00185         '''
00186         Allows this workflow to
00187         include actions to be displayed in the actions box.
00188         Called only when this workflow is applicable to
00189         info.object.
00190         Returns the actions to be displayed to the user.
00191         '''
00192         ob = info.object
00193         sdef = self._getWorkflowStateOf(ob)
00194         if sdef is None:
00195             return None
00196         res = []
00197         for tid in sdef.transitions:
00198             tdef = self.transitions.get(tid, None)
00199             if tdef is not None and tdef.trigger_type == TRIGGER_USER_ACTION:
00200                 if tdef.actbox_name:
00201                     if self._checkTransitionGuard(tdef, ob):
00202                         res.append((tid, {
00203                             'id': tid,
00204                             'name': tdef.actbox_name % info,
00205                             'url': tdef.actbox_url % info,
00206                             'permissions': (),  # Predetermined.
00207                             'category': tdef.actbox_category,
00208                             'transition': tdef}))
00209         res.sort()
00210         return [ result[1] for result in res ]
00211 
00212     security.declarePrivate('listGlobalActions')
00213     def listGlobalActions(self, info):
00214         '''
00215         Allows this workflow to
00216         include actions to be displayed in the actions box.
00217         Called on every request.
00218         Returns the actions to be displayed to the user.
00219         '''
00220         if not self.worklists:
00221             return None  # Optimization
00222         sm = getSecurityManager()
00223         portal = self._getPortalRoot()
00224         res = []
00225         fmt_data = None
00226         for id, qdef in self.worklists.items():
00227             if qdef.actbox_name:
00228                 guard = qdef.guard
00229                 if guard is None or guard.check(sm, self, portal):
00230                     searchres = None
00231                     var_match_keys = qdef.getVarMatchKeys()
00232                     if var_match_keys:
00233                         # Check the catalog for items in the worklist.
00234                         catalog = getToolByName(self, 'portal_catalog')
00235                         kw = {}
00236                         for k in var_match_keys:
00237                             v = qdef.getVarMatch(k)
00238                             kw[k] = [ x % info for x in v ]
00239                         searchres = catalog.searchResults(**kw)
00240                         if not searchres:
00241                             continue
00242                     if fmt_data is None:
00243                         fmt_data = TemplateDict()
00244                         fmt_data._push(info)
00245                     fmt_data._push({'count': len(searchres)})
00246                     res.append((id, {'id': id,
00247                                      'name': qdef.actbox_name % fmt_data,
00248                                      'url': qdef.actbox_url % fmt_data,
00249                                      'permissions': (),  # Predetermined.
00250                                      'category': qdef.actbox_category}))
00251                     fmt_data._pop()
00252         res.sort()
00253         return [ result[1] for result in res ]
00254 
00255     security.declarePrivate('isActionSupported')
00256     def isActionSupported(self, ob, action, **kw):
00257         '''
00258         Returns a true value if the given action name
00259         is possible in the current state.
00260         '''
00261         sdef = self._getWorkflowStateOf(ob)
00262         if sdef is None:
00263             return 0
00264         if action in sdef.transitions:
00265             tdef = self.transitions.get(action, None)
00266             if (tdef is not None and
00267                 tdef.trigger_type == TRIGGER_USER_ACTION and
00268                 self._checkTransitionGuard(tdef, ob, **kw)):
00269                 return 1
00270         return 0
00271 
00272     security.declarePrivate('doActionFor')
00273     def doActionFor(self, ob, action, comment='', **kw):
00274         '''
00275         Allows the user to request a workflow action.  This method
00276         must perform its own security checks.
00277         '''
00278         kw['comment'] = comment
00279         sdef = self._getWorkflowStateOf(ob)
00280         if sdef is None:
00281             raise WorkflowException(_(u'Object is in an undefined state.'))
00282         if action not in sdef.transitions:
00283             raise Unauthorized(action)
00284         tdef = self.transitions.get(action, None)
00285         if tdef is None or tdef.trigger_type != TRIGGER_USER_ACTION:
00286             msg = _(u"Transition '${action_id}' is not triggered by a user "
00287                     u"action.", mapping={'action_id': action})
00288             raise WorkflowException(msg)
00289         if not self._checkTransitionGuard(tdef, ob, **kw):
00290             raise Unauthorized(action)
00291         self._changeStateOf(ob, tdef, kw)
00292 
00293     security.declarePrivate('isInfoSupported')
00294     def isInfoSupported(self, ob, name):
00295         '''
00296         Returns a true value if the given info name is supported.
00297         '''
00298         if name == self.state_var:
00299             return 1
00300         vdef = self.variables.get(name, None)
00301         if vdef is None:
00302             return 0
00303         return 1
00304 
00305     security.declarePrivate('getInfoFor')
00306     def getInfoFor(self, ob, name, default):
00307         '''
00308         Allows the user to request information provided by the
00309         workflow.  This method must perform its own security checks.
00310         '''
00311         if name == self.state_var:
00312             return self._getWorkflowStateOf(ob, 1)
00313         vdef = self.variables[name]
00314         if vdef.info_guard is not None and not vdef.info_guard.check(
00315             getSecurityManager(), self, ob):
00316             return default
00317         status = self._getStatusOf(ob)
00318         if status is not None and status.has_key(name):
00319             value = status[name]
00320 
00321         # Not set yet.  Use a default.
00322         elif vdef.default_expr is not None:
00323             ec = createExprContext(StateChangeInfo(ob, self, status))
00324             value = vdef.default_expr(ec)
00325         else:
00326             value = vdef.default_value
00327 
00328         return value
00329 
00330     security.declarePrivate('allowCreate')
00331     def allowCreate(self, container, type_name):
00332         """Returns true if the user is allowed to create a workflow instance.
00333 
00334         The object passed to the guard is the prospective container.
00335         """
00336         if self.creation_guard is not None:
00337             return self.creation_guard.check(
00338                 getSecurityManager(), self, container)
00339         return 1
00340 
00341     security.declarePrivate('notifyCreated')
00342     def notifyCreated(self, ob):
00343         """Notifies this workflow after an object has been created and added.
00344         """
00345         try:
00346             self._changeStateOf(ob, None)
00347         except ( ObjectDeleted, ObjectMoved ):
00348             # Swallow.
00349             pass
00350 
00351     security.declarePrivate('notifyBefore')
00352     def notifyBefore(self, ob, action):
00353         '''
00354         Notifies this workflow of an action before it happens,
00355         allowing veto by exception.  Unless an exception is thrown, either
00356         a notifySuccess() or notifyException() can be expected later on.
00357         The action usually corresponds to a method name.
00358         '''
00359         pass
00360 
00361     security.declarePrivate('notifySuccess')
00362     def notifySuccess(self, ob, action, result):
00363         '''
00364         Notifies this workflow that an action has taken place.
00365         '''
00366         pass
00367 
00368     security.declarePrivate('notifyException')
00369     def notifyException(self, ob, action, exc):
00370         '''
00371         Notifies this workflow that an action failed.
00372         '''
00373         pass
00374 
00375     security.declarePrivate('updateRoleMappingsFor')
00376     def updateRoleMappingsFor(self, ob):
00377         """Changes the object permissions according to the current state.
00378         """
00379         changed = 0
00380         sdef = self._getWorkflowStateOf(ob)
00381         if sdef is None:
00382             return 0
00383         # Update the role -> permission map.
00384         if self.permissions:
00385             for p in self.permissions:
00386                 roles = []
00387                 if sdef.permission_roles is not None:
00388                     roles = sdef.permission_roles.get(p, roles)
00389                 if modifyRolesForPermission(ob, p, roles):
00390                     changed = 1
00391         # Update the group -> role map.
00392         groups = self.getGroups()
00393         managed_roles = self.getRoles()
00394         if groups and managed_roles:
00395             for group in groups:
00396                 roles = ()
00397                 if sdef.group_roles is not None:
00398                     roles = sdef.group_roles.get(group, ())
00399                 if modifyRolesForGroup(ob, group, roles, managed_roles):
00400                     changed = 1
00401         return changed
00402 
00403     def _checkTransitionGuard(self, t, ob, **kw):
00404         guard = t.guard
00405         if guard is None:
00406             return 1
00407         if guard.check(getSecurityManager(), self, ob, **kw):
00408             return 1
00409         return 0
00410 
00411     def _findAutomaticTransition(self, ob, sdef):
00412         tdef = None
00413         for tid in sdef.transitions:
00414             t = self.transitions.get(tid, None)
00415             if t is not None and t.trigger_type == TRIGGER_AUTOMATIC:
00416                 if self._checkTransitionGuard(t, ob):
00417                     tdef = t
00418                     break
00419         return tdef
00420 
00421     def _changeStateOf(self, ob, tdef=None, kwargs=None):
00422         '''
00423         Changes state.  Can execute multiple transitions if there are
00424         automatic transitions.  tdef set to None means the object
00425         was just created.
00426         '''
00427         moved_exc = None
00428         while 1:
00429             try:
00430                 sdef = self._executeTransition(ob, tdef, kwargs)
00431             except ObjectMoved, moved_exc:
00432                 ob = moved_exc.getNewObject()
00433                 sdef = self._getWorkflowStateOf(ob)
00434                 # Re-raise after all transitions.
00435             if sdef is None:
00436                 break
00437             tdef = self._findAutomaticTransition(ob, sdef)
00438             if tdef is None:
00439                 # No more automatic transitions.
00440                 break
00441             # Else continue.
00442         if moved_exc is not None:
00443             # Re-raise.
00444             raise moved_exc
00445 
00446     def _executeTransition(self, ob, tdef=None, kwargs=None):
00447         '''
00448         Private method.
00449         Puts object in a new state.
00450         '''
00451         sci = None
00452         econtext = None
00453         moved_exc = None
00454 
00455         # Figure out the old and new states.
00456         old_sdef = self._getWorkflowStateOf(ob)
00457         old_state = old_sdef.getId()
00458         if tdef is None:
00459             new_state = self.initial_state
00460             former_status = {}
00461         else:
00462             new_state = tdef.new_state_id
00463             if not new_state:
00464                 # Stay in same state.
00465                 new_state = old_state
00466             former_status = self._getStatusOf(ob)
00467         new_sdef = self.states.get(new_state, None)
00468         if new_sdef is None:
00469             msg = _(u'Destination state undefined: ${state_id}',
00470                     mapping={'state_id': new_state})
00471             raise WorkflowException(msg)
00472 
00473         # Fire "before" event
00474         notify(BeforeTransitionEvent(ob, self, old_sdef, new_sdef, tdef, former_status, kwargs))
00475 
00476         # Execute the "before" script.
00477         if tdef is not None and tdef.script_name:
00478             script = self.scripts[tdef.script_name]
00479             # Pass lots of info to the script in a single parameter.
00480             sci = StateChangeInfo(
00481                 ob, self, former_status, tdef, old_sdef, new_sdef, kwargs)
00482             try:
00483                 script(sci)  # May throw an exception.
00484             except ObjectMoved, moved_exc:
00485                 ob = moved_exc.getNewObject()
00486                 # Re-raise after transition
00487 
00488         # Update variables.
00489         state_values = new_sdef.var_values
00490         if state_values is None: state_values = {}
00491         tdef_exprs = None
00492         if tdef is not None: tdef_exprs = tdef.var_exprs
00493         if tdef_exprs is None: tdef_exprs = {}
00494         status = {}
00495         for id, vdef in self.variables.items():
00496             if not vdef.for_status:
00497                 continue
00498             expr = None
00499             if state_values.has_key(id):
00500                 value = state_values[id]
00501             elif tdef_exprs.has_key(id):
00502                 expr = tdef_exprs[id]
00503             elif not vdef.update_always and former_status.has_key(id):
00504                 # Preserve former value
00505                 value = former_status[id]
00506             else:
00507                 if vdef.default_expr is not None:
00508                     expr = vdef.default_expr
00509                 else:
00510                     value = vdef.default_value
00511             if expr is not None:
00512                 # Evaluate an expression.
00513                 if econtext is None:
00514                     # Lazily create the expression context.
00515                     if sci is None:
00516                         sci = StateChangeInfo(
00517                             ob, self, former_status, tdef,
00518                             old_sdef, new_sdef, kwargs)
00519                     econtext = createExprContext(sci)
00520                 value = expr(econtext)
00521             status[id] = value
00522 
00523         # Update state.
00524         status[self.state_var] = new_state
00525         tool = aq_parent(aq_inner(self))
00526         tool.setStatusOf(self.id, ob, status)
00527 
00528         # Update role to permission assignments.
00529         self.updateRoleMappingsFor(ob)
00530 
00531         # Execute the "after" script.
00532         if tdef is not None and tdef.after_script_name:
00533             script = self.scripts[tdef.after_script_name]
00534             # Pass lots of info to the script in a single parameter.
00535             sci = StateChangeInfo(
00536                 ob, self, status, tdef, old_sdef, new_sdef, kwargs)
00537             script(sci)  # May throw an exception.
00538 
00539         # Fire "after" event
00540         notify(AfterTransitionEvent(ob, self, old_sdef, new_sdef, tdef, status, kwargs))
00541 
00542         # Return the new state object.
00543         if moved_exc is not None:
00544             # Propagate the notification that the object has moved.
00545             raise moved_exc
00546         else:
00547             return new_sdef
00548 
00549 InitializeClass(DCWorkflowDefinition)