InteractionWorkflow.py 19 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.
#
##############################################################################

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

35 36 37 38
# show as xml library
from lxml import etree
from lxml.etree import Element, SubElement

39 40
_MARKER = []

Jean-Paul Smets's avatar
Jean-Paul Smets committed
41
class InteractionWorkflowDefinition (DCWorkflowDefinition, ActiveObject):
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
  """
  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.)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
57

58
  The transformation template is defined as:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
59

60
  - pre method executed before
Jean-Paul Smets's avatar
Jean-Paul Smets committed
61

62
  - pre async executed anyway
Jean-Paul Smets's avatar
Jean-Paul Smets committed
63

64
  - post method executed after success before return
Jean-Paul Smets's avatar
Jean-Paul Smets committed
65

66
  - post method executed after success anyway
Jean-Paul Smets's avatar
Jean-Paul Smets committed
67

68 69 70 71
  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.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
72

73
  Examples of applications:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
74

75
  - when movement is updated, apply transformation rules to movement
Jean-Paul Smets's avatar
Jean-Paul Smets committed
76

77
  - when stock is 0, post an event of stock empty
Jean-Paul Smets's avatar
Jean-Paul Smets committed
78

79
  - when birthday is called, call the happy birthday script
Jean-Paul Smets's avatar
Jean-Paul Smets committed
80

81 82 83
  ERP5 main application: specialize behaviour of classes "on the fly".
  Make the architecture as modular as possible. Implement connections
  a la Qt.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
84

85
  Try to mimic: Workflow...
Jean-Paul Smets's avatar
Jean-Paul Smets committed
86

87
  Question: should be use it for values ? or use a global value model ?
Jean-Paul Smets's avatar
Jean-Paul Smets committed
88

89
  Status : OK
Jean-Paul Smets's avatar
Jean-Paul Smets committed
90 91


92
  Implementation:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
93

94 95 96 97 98
  A new kind of workflow (stateless). Follow the DCWorkflow class.
  Provide filters (per portal_type, etc.). Allow inspection of objects ?
  """
  meta_type = 'Workflow'
  title = 'Interaction Workflow Definition'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
99

100
  interactions = None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
101

102
  security = ClassSecurityInfo()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
103

104 105 106 107 108 109
  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
Jean-Paul Smets's avatar
Jean-Paul Smets committed
110

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
  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'))

  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

  security.declarePrivate('listObjectActions')
  def listObjectActions(self, info):
    return []

  security.declarePrivate('_changeStateOf')
  def _changeStateOf(self, ob, tdef=None, kwargs=None) :
    """
    InteractionWorkflow is stateless. Thus, this function should do nothing.
    """
    return

  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.
    '''
    vdef = self.variables.get(name, _MARKER)
    if vdef is _MARKER:
      return default
    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

  security.declarePrivate('isWorkflowMethodSupported')
  def isWorkflowMethodSupported(self, ob, method_id):
    '''
182 183
    Returns a true value if the given workflow method
    is supported in the current state.
184 185
    '''
    tdef = self.interactions.get(method_id, None)
186
    return tdef is not None and self._checkTransitionGuard(tdef, ob)
187 188 189 190 191 192 193 194 195 196 197

  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.
    '''
    return

  security.declarePrivate('notifyWorkflowMethod')
  def notifyWorkflowMethod(self, ob, transition_list, args=None, kw=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
198
    """
199 200 201 202 203 204 205 206 207 208 209 210 211
    InteractionWorkflow is stateless. Thus, this function should do nothing.
    """
    return

  security.declarePrivate('notifyBefore')
  def notifyBefore(self, ob, transition_list, args=None, kw=None):
    '''
    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.
    '''
    if type(transition_list) in StringTypes:
212 213
      return

214 215 216 217 218 219 220 221 222 223 224 225 226
    # Wrap args into kw since this is the only way
    # to be compatible with DCWorkflow
    # A better approach consists in extending DCWorkflow
    if kw is None:
      kw = {'workflow_method_args' : args}
    else:
      kw = kw.copy()
      kw['workflow_method_args'] = args
    filtered_transition_list = []

    for t_id in transition_list:
      tdef = self.interactions[t_id]
      assert tdef.trigger_type == TRIGGER_WORKFLOW_METHOD
227 228 229 230 231 232 233 234 235
      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
236 237

    return filtered_transition_list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
238

239 240 241 242 243 244
  security.declarePrivate('notifySuccess')
  def notifySuccess(self, ob, transition_list, result, args=None, kw=None):
    '''
    Notifies this workflow that an action has taken place.
    '''
    if type(transition_list) in StringTypes:
245 246
      return

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
    kw = kw.copy()
    kw['workflow_method_args'] = args
    kw['workflow_method_result'] = result

    for t_id in transition_list:
      tdef = self.interactions[t_id]
      assert tdef.trigger_type == TRIGGER_WORKFLOW_METHOD

      # 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]
273
        else:
274 275 276 277 278 279 280 281 282
          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:
283
              sci = StateChangeInfo(
284 285 286 287 288 289 290 291 292 293 294 295 296 297
                  ob, self, former_status, tdef,
                  None, None, None)
            econtext = createExprContext(sci)
          value = expr(econtext)
        status[id] = value

      sci = StateChangeInfo(
            ob, self, former_status, tdef, None, None, kwargs=kw)
      # 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

298 299
      # Queue the "Before Commit" scripts
      sm = getSecurityManager()
300 301
      for script_name in tdef.before_commit_script_name:
        transaction.get().addBeforeCommitHook(self._before_commit,
302
                                              (sci, script_name, sm))
303 304 305 306 307 308 309

      # Execute "activity" scripts
      for script_name in tdef.activate_script_name:
        self.activate(activity='SQLQueue')\
            .activeScript(script_name, ob.getRelativeUrl(),
                          status, tdef.id)

310
  def _before_commit(self, sci, script_name, security_manager):
311 312 313 314 315 316
    # check the object still exists before calling the script
    ob = sci.object
    while ob.isTempObject():
      ob = ob.getParentValue()
    if aq_base(self.unrestrictedTraverse(ob.getPhysicalPath(), None)) is \
       aq_base(ob):
317 318 319 320 321 322 323 324 325
      current_security_manager = getSecurityManager()
      try:
        # Who knows what happened to the authentication context
        # between here and when the interaction was executed... So we
        # need to switch to the security manager as it was back then
        setSecurityManager(security_manager)
        self.scripts[script_name](sci)
      finally:
        setSecurityManager(current_security_manager)
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350

  security.declarePrivate('activeScript')
  def activeScript(self, script_name, ob_url, status, tdef_id):
    script = self.scripts[script_name]
    ob = self.unrestrictedTraverse(ob_url)
    tdef = self.interactions.get(tdef_id)
    sci = StateChangeInfo(
                  ob, self, status, tdef, None, None, None)
    script(sci)

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

  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)
351

352 353 354
  def getReference(self):
    return self.id

355
  def getTransitionValueList(self):
356
    if self.interactions is not None:
357 358 359 360 361 362
      return self.interactions
    return None

  def getTransitionIdList(self):
    if self.interactions is not None:
      return self.interactions.objectIds()
363 364
    return None

365 366 367
  def getPortalType(self):
    return self.__class__.__name__

368 369 370 371
  def showAsXML(self, root=None):
    if root is None:
      root = Element('erp5')
      return_as_object = False
372
    interaction_workflow_prop_id_to_show = {
373 374 375 376 377 378 379
          'description':'text', 'manager_bypass':'int'}
    interaction_workflow = SubElement(root, 'interaction_workflow',
                        attrib=dict(reference=self.getReference(),
                        portal_type='Interaction Workflow'))

    for prop_id in sorted(interaction_workflow_prop_id_to_show):
      prop_value = self.__dict__[prop_id]
380 381
      if prop_value is None or prop_value == [] or prop_value == ():
        prop_value = ''
382 383 384 385 386
      prop_type = interaction_workflow_prop_id_to_show[prop_id]
      sub_object = SubElement(interaction_workflow, prop_id, attrib=dict(type=prop_type))
      sub_object.text = str(prop_value)

    # 1. Interaction as XML
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
    interaction_reference_list = []
    interaction_id_list = sorted(self.interactions.keys())
    interaction_prop_id_to_show = {'actbox_category':'string', 'actbox_url':'string',
    'actbox_name':'string', 'activate_script_name':'string',
    'after_script_name':'string', 'before_commit_script_name':'string',
    'description':'text', 'guard':'object', 'method_id':'string',
    'once_per_transaction':'string', 'portal_type_filter':'string',
    'portal_type_group_filter':'string', 'script_name':'string',
    'temporary_document_disallowed':'string', 'trigger_type':'string'}
    for tid in interaction_id_list:
      interaction_reference_list.append(tid)
    interactions = SubElement(interaction_workflow, 'interactions', attrib=dict(
      interaction_list=str(interaction_reference_list),
      number_of_element=str(len(interaction_reference_list))))
    for tid in interaction_id_list:
      tdef = self.interactions[tid]
      interaction = SubElement(interactions, 'interaction', attrib=dict(
            reference=tdef.getReference(),portal_type='Interaction'))
      guard = SubElement(interaction, 'guard', attrib=dict(type='object'))
      for property_id in sorted(interaction_prop_id_to_show):
        # creationg guard
        if property_id == 'guard':
          for prop_id in sorted(['groups', 'permissions', 'expr', 'roles']):
            guard_obj = getattr(tdef, 'guard')
            if guard_obj is not None:
412
              if prop_id in guard_obj.__dict__:
413 414 415
                if prop_id == 'expr':
                  prop_value =  getattr(guard_obj.expr, 'text', '')
                else: prop_value = guard_obj.__dict__[prop_id]
416 417
              else:
                prop_value = ''
418 419 420
            else:
              prop_value = ''
            guard_config = SubElement(guard, prop_id, attrib=dict(type='guard configuration'))
421
            if prop_value is None or prop_value == () or prop_value == []:
422 423 424 425 426 427 428
              prop_value = ''
            guard_config.text = str(prop_value)
        # no-property definded action box configuration
        elif property_id in sorted(['actbox_name', 'actbox_url', 'actbox_category']):
          property_value = getattr(tdef, property_id, None)
          sub_object = SubElement(interaction, property_id, attrib=dict(type='string'))
        else:
429 430 431 432
          if property_id in tdef.__dict__:
            property_value = tdef.__dict__[property_id]
          else:
            property_value = ''
433 434 435 436
          property_type = interaction_prop_id_to_show[property_id]
          sub_object = SubElement(interaction, property_id, attrib=dict(type=property_type))
        if property_value is None or property_value == [] or property_value == ():
          property_value = ''
437 438 439
        if property_id in ['once_per_transaction', 'temporary_document_disallowed']:
          if property_value == True:
            property_value = '1'
440 441
          elif property_value == False or property_value is '':
            property_value = '0'
442
        sub_object.text = str(property_value)
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465

    # 2. Variable as XML
    variable_reference_list = []
    variable_id_list = sorted(self.variables.keys())
    variable_prop_id_to_show = {'description':'text',
          'default_expr':'string', 'for_catalog':'int', 'for_status':'int',
          'update_always':'int'}
    for vid in variable_id_list:
      variable_reference_list.append(vid)
    variables = SubElement(interaction_workflow, 'variables', attrib=dict(variable_list=str(variable_reference_list),
                        number_of_element=str(len(variable_reference_list))))
    for vid in variable_id_list:
      vdef = self.variables[vid]
      variable = SubElement(variables, 'variable', attrib=dict(reference=vdef.getReference(),
            portal_type='Variable'))
      for property_id in sorted(variable_prop_id_to_show):
        if property_id == 'default_expr':
          expression = getattr(vdef, property_id, None)
          if expression is not None:
            property_value = expression.text
          else:
            property_value = ''
        else:
466 467 468 469 470
          property_value = getattr(vdef, property_id, '')
        if property_value is None or property_value == [] or property_value ==():
          property_value = ''
        property_type = variable_prop_id_to_show[property_id]
        sub_object = SubElement(variable, property_id, attrib=dict(type=property_type))
471 472
        sub_object.text = str(property_value)

473
    # 3. Script as XML
474 475
    script_reference_list = []
    script_id_list = sorted(self.scripts.keys())
476 477
    script_prop_id_to_show = {'body':'string', 'parameter_signature':'string',
          'proxy_roles':'tokens'}
478 479 480 481 482 483 484 485 486 487 488 489 490
    for sid in script_id_list:
      script_reference_list.append(sid)
    scripts = SubElement(interaction_workflow, 'scripts', attrib=dict(script_list=str(script_reference_list),
                        number_of_element=str(len(script_reference_list))))
    for sid in script_id_list:
      sdef = self.scripts[sid]
      script = SubElement(scripts, 'script', attrib=dict(reference=sid,
        portal_type='Workflow Script'))
      for property_id in sorted(script_prop_id_to_show):
        if property_id == 'body':
          property_value = sdef.getBody()
        elif property_id == 'parameter_signature':
          property_value = sdef.getParams()
491 492
        elif property_id == 'proxy_roles':
          property_value = sdef.getProxyRole()
493 494 495 496
        else:
          property_value = getattr(sdef, property_id)
        property_type = script_prop_id_to_show[property_id]
        sub_object = SubElement(script, property_id, attrib=dict(type=property_type))
497 498
        if property_value is None or property_value == [] or property_value == ():
          property_value = ''
499 500 501 502 503 504 505 506
        sub_object.text = str(property_value)

    # return xml object
    if return_as_object:
      return root
    return etree.tostring(root, encoding='utf-8',
                          xml_declaration=True, pretty_print=True)

Jean-Paul Smets's avatar
Jean-Paul Smets committed
507 508 509
Globals.InitializeClass(InteractionWorkflowDefinition)

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