InteractionWorkflow.py 12.9 KB
Newer Older
Nicolas Delaby's avatar
Nicolas Delaby committed
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
##############################################################################
#
# Copyright (c) 2003 Nexedi SARL and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
##############################################################################

Jérome Perrin's avatar
Jérome Perrin committed
20
import transaction
21
from Products.ERP5Type import Globals
Jean-Paul Smets's avatar
Jean-Paul Smets committed
22
import App
23
from types import StringTypes
Jean-Paul Smets's avatar
Jean-Paul Smets committed
24
from AccessControl import getSecurityManager, ClassSecurityInfo
25
from Products.CMFCore.utils import getToolByName
Jean-Paul Smets's avatar
Jean-Paul Smets committed
26
from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
27
from Products.DCWorkflow.Transitions import TRIGGER_WORKFLOW_METHOD
Jean-Paul Smets's avatar
Jean-Paul Smets committed
28
from Products.DCWorkflow.Expression import StateChangeInfo, createExprContext
29
from Products.ERP5Type.Workflow import addWorkflowFactory
Jean-Paul Smets's avatar
Jean-Paul Smets committed
30
from Products.CMFActivity.ActiveObject import ActiveObject
31
from Products.ERP5Type import Permissions
Jean-Paul Smets's avatar
Jean-Paul Smets committed
32

33 34
_MARKER = []

Jean-Paul Smets's avatar
Jean-Paul Smets committed
35
class InteractionWorkflowDefinition (DCWorkflowDefinition, ActiveObject):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
    """
    The InteractionTool implements portal object
    interaction policies.

    An interaction is defined by
    a domain and a behaviour:

    The domain is defined as:

    - the meta_type it applies to

    - the portal_type it applies to

    - the conditions of application (category membership, value range,
      security, function, etc.)

    The transformation template is defined as:

    - pre method executed before

    - pre async executed anyway

    - post method executed after success before return

    - post method executed after success anyway

    This is similar to signals and slots except is applies to classes
    rather than instances. Similar to
    stateless workflow methods with more options. Similar to ZSQL scipts
    but in more cases.

    Examples of applications:

    - when movement is updated, apply transformation rules to movement

    - when stock is 0, post an event of stock empty

    - when birthday is called, call the happy birthday script
    
    ERP5 main application: specialize behaviour of classes "on the fly".
    Make the architecture as modular as possible. Implement connections
Romain Courteaud's avatar
Romain Courteaud committed
77
    a la Qt.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
78 79 80 81 82 83 84 85 86 87 88 89 90

    Try to mimic: Workflow...

    Question: should be use it for values ? or use a global value model ?

    Status : OK


    Implementation:

    A new kind of workflow (stateless). Follow the DCWorkflow class.
    Provide filters (per portal_type, etc.). Allow inspection of objects ?
    """
91
    meta_type = 'Workflow'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    title = 'Interaction Workflow Definition'

    interactions = None

    security = ClassSecurityInfo()

    manage_options = (
        {'label': 'Properties', 'action': 'manage_properties'},
        {'label': 'Interactions', 'action': 'interactions/manage_main'},
        {'label': 'Variables', 'action': 'variables/manage_main'},
        {'label': 'Scripts', 'action': 'scripts/manage_main'},
        ) + App.Undo.UndoSupport.manage_options

    def __init__(self, id):
        self.id = id
        from Interaction import Interaction
        self._addObject(Interaction('interactions'))
        from Products.DCWorkflow.Variables import Variables
        self._addObject(Variables('variables'))
        from Products.DCWorkflow.Worklists import Worklists
        self._addObject(Worklists('worklists'))
        from Products.DCWorkflow.Scripts import Scripts
        self._addObject(Scripts('scripts'))

116 117 118 119 120 121 122 123 124 125 126 127
    security.declareProtected(Permissions.View, 'getChainedPortalTypeList')
    def getChainedPortalTypeList(self):
        """Returns the list of portal types that are chained to this
        interaction workflow."""
        chained_ptype_list = []
        wf_tool = getToolByName(self, 'portal_workflow')
        types_tool = getToolByName(self, 'portal_types')
        for ptype in types_tool.objectIds():
            if self.getId() in wf_tool._chains_by_type.get(ptype, []) :
                chained_ptype_list.append(ptype)
        return chained_ptype_list
      
Jean-Paul Smets's avatar
Jean-Paul Smets committed
128 129 130 131
    security.declarePrivate('listObjectActions')
    def listObjectActions(self, info):
        return []

132 133 134 135 136 137 138
    security.declarePrivate('_changeStateOf')
    def _changeStateOf(self, ob, tdef=None, kwargs=None) :
      """
      InteractionWorkflow is stateless. Thus, this function should do nothing.
      """
      return

Jean-Paul Smets's avatar
Jean-Paul Smets committed
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
    security.declarePrivate('isInfoSupported')
    def isInfoSupported(self, ob, name):
        '''
        Returns a true value if the given info name is supported.
        '''
        vdef = self.variables.get(name, None)
        if vdef is None:
            return 0
        return 1
    
    security.declarePrivate('getInfoFor')
    def getInfoFor(self, ob, name, default):
        '''
        Allows the user to request information provided by the
        workflow.  This method must perform its own security checks.
        '''
155 156 157
        vdef = self.variables.get(name, _MARKER)
        if vdef is _MARKER:
          return default
Jean-Paul Smets's avatar
Jean-Paul Smets committed
158 159 160 161 162 163 164 165 166 167 168 169 170 171
        if vdef.info_guard is not None and not vdef.info_guard.check(
            getSecurityManager(), self, ob):
            return default
        status = self._getStatusOf(ob)
        if status is not None and status.has_key(name):
            value = status[name]
        # Not set yet.  Use a default.
        elif vdef.default_expr is not None:
            ec = createExprContext(StateChangeInfo(ob, self, status))
            value = vdef.default_expr(ec)
        else:
            value = vdef.default_value

        return value
172

Jean-Paul Smets's avatar
Jean-Paul Smets committed
173 174 175 176 177
    security.declarePrivate('isWorkflowMethodSupported')
    def isWorkflowMethodSupported(self, ob, method_id):
        '''
        Returns a true value if the given workflow is 
        automatic with the propper method_id
178 179 180

        NOTE: this method is not used in ERP5 because
        of transition_list approach
Jean-Paul Smets's avatar
Jean-Paul Smets committed
181
        '''
182 183 184 185 186
        tdef = self.interactions.get(method_id, None)
        if (tdef is not None and
          tdef.trigger_type == TRIGGER_WORKFLOW_METHOD and
          self._checkTransitionGuard(tdef, ob)):
          return 1
187
        return 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
188 189 190 191 192 193 194 195


    security.declarePrivate('wrapWorkflowMethod')
    def wrapWorkflowMethod(self, ob, method_id, func, args, kw):
        '''
        Allows the user to request a workflow action.  This method
        must perform its own security checks.
        '''
Sebastien Robin's avatar
Sebastien Robin committed
196
        return
Jean-Paul Smets's avatar
Jean-Paul Smets committed
197

198
    security.declarePrivate('notifyWorkflowMethod')
199
    def notifyWorkflowMethod(self, ob, transition_list, args=None, kw=None):
200 201 202 203 204
      """
      InteractionWorkflow is stateless. Thus, this function should do nothing.
      """
      return

Jean-Paul Smets's avatar
Jean-Paul Smets committed
205
    security.declarePrivate('notifyBefore')
206
    def notifyBefore(self, ob, transition_list, args=None, kw=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
207 208 209 210 211 212
        '''
        Notifies this workflow of an action before it happens,
        allowing veto by exception.  Unless an exception is thrown, either
        a notifySuccess() or notifyException() can be expected later on.
        The action usually corresponds to a method name.
        '''
213 214 215
        if type(transition_list) in StringTypes:
          return

216 217 218
        # Wrap args into kw since this is the only way
        # to be compatible with DCWorkflow
        # A better approach consists in extending DCWorkflow
219 220 221 222 223
        if kw is None:
          kw = {'workflow_method_args' : args}
        else:
          kw = kw.copy()
          kw['workflow_method_args'] = args
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
        filtered_transition_list = []

        for t_id in transition_list:
          tdef = self.interactions[t_id]
          if tdef.trigger_type == TRIGGER_WORKFLOW_METHOD:
            if (tdef.portal_type_filter is None or \
                ob.getPortalType() in tdef.portal_type_filter) and \
                self._checkTransitionGuard(tdef, ob, **kw):
              filtered_transition_list.append(tdef.id)
              former_status = self._getStatusOf(ob)
              # Execute the "before" script.
              for script_name in tdef.script_name:
                script = self.scripts[script_name]
                # Pass lots of info to the script in a single parameter.
                sci = StateChangeInfo(
                    ob, self, former_status, tdef, None, None, kwargs=kw)
                script(sci)  # May throw an exception

        return filtered_transition_list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
243 244

    security.declarePrivate('notifySuccess')
245
    def notifySuccess(self, ob, transition_list, result, args=None, kw=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
246 247 248
        '''
        Notifies this workflow that an action has taken place.
        '''
249 250
        if type(transition_list) in StringTypes:
          return
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295

        kw = kw.copy()
        kw['workflow_method_args'] = args
        kw['workflow_method_result'] = result

        for t_id in transition_list:
          tdef = self.interactions[t_id]
          if tdef.trigger_type == TRIGGER_WORKFLOW_METHOD:
            if (tdef.portal_type_filter is None or \
                ob.getPortalType() in tdef.portal_type_filter):
              # Initialize variables
              former_status = self._getStatusOf(ob)
              econtext = None
              sci = None

              # Update variables.
              tdef_exprs = tdef.var_exprs
              if tdef_exprs is None: tdef_exprs = {}
              status = {}
              for id, vdef in self.variables.items():
                if not vdef.for_status:
                  continue
                expr = None
                if tdef_exprs.has_key(id):
                  expr = tdef_exprs[id]
                elif not vdef.update_always and former_status.has_key(id):
                  # Preserve former value
                  value = former_status[id]
                else:
                  if vdef.default_expr is not None:
                    expr = vdef.default_expr
                  else:
                    value = vdef.default_value
                if expr is not None:
                  # Evaluate an expression.
                  if econtext is None:
                    # Lazily create the expression context.
                    if sci is None:
                      sci = StateChangeInfo(
                          ob, self, former_status, tdef,
                          None, None, None)
                    econtext = createExprContext(sci)
                  value = expr(econtext)
                status[id] = value

Nicolas Delaby's avatar
Nicolas Delaby committed
296 297
              sci = StateChangeInfo(
                    ob, self, former_status, tdef, None, None, kwargs=kw)
298 299 300 301 302 303
              # Execute the "after" script.
              for script_name in tdef.after_script_name:
                script = self.scripts[script_name]
                # Pass lots of info to the script in a single parameter.
                script(sci)  # May throw an exception

304 305
              # Execute Before Commit
              for script_name in tdef.before_commit_script_name:
306 307 308
                del sci.object
                transaction.get().addBeforeCommitHook(self._before_commit,
                  (sci, ob.getPhysicalPath(), script_name))
309

310 311 312
              # Execute "activity" scripts
              for script_name in tdef.activate_script_name:
                self.activate(activity='SQLQueue')\
Nicolas Delaby's avatar
Nicolas Delaby committed
313
                    .activeScript(script_name, ob.getRelativeUrl(),
314
                                  status, tdef.id)
315

316 317 318 319 320 321 322
    def _before_commit(self, sci, path, script_name):
      try:
        sci.object = self.unrestrictedTraverse(path)
      except KeyError:
        return
      self.scripts[script_name](sci)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
323
    security.declarePrivate('activeScript')
324
    def activeScript(self, script_name, ob_url, status, tdef_id):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
325
          script = self.scripts[script_name]
326
          ob = self.unrestrictedTraverse(ob_url)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
327 328
          tdef = self.interactions.get(tdef_id)
          sci = StateChangeInfo(
329
                        ob, self, status, tdef, None, None, None)
330
          script(sci)
331

332 333
    def _getWorkflowStateOf(self, ob, id_only=0):
          return None
334

335 336 337 338 339 340 341 342 343 344 345 346 347
    def _checkTransitionGuard(self, t, ob, **kw):
        # This check can be implemented with a guard expression, but
        # it has a lot of overhead to use a TALES, so we make a special
        # treatment for the frequent case, that is, disallow the trigger
        # on a temporary document.
        if t.temporary_document_disallowed:
            isTempDocument = getattr(ob, 'isTempDocument', None)
            if isTempDocument is not None:
                if isTempDocument():
                    return 0

        return DCWorkflowDefinition._checkTransitionGuard(self, t, ob, **kw)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
348 349 350
Globals.InitializeClass(InteractionWorkflowDefinition)

addWorkflowFactory(InteractionWorkflowDefinition, id='interaction_workflow',
351
                   title='Web-configurable interaction workflow')