Workflow.py 46.8 KB
Newer Older
1 2 3 4
##############################################################################
#
# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
#                    Romain Courteaud <romain@nexedi.com>
5
#               2014 Wenjie Zheng <wenjie.zheng@tiolive.com>
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

29 30
import os
import sys
31

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
from AccessControl import ClassSecurityInfo
from AccessControl.unauthorized import Unauthorized
from AccessControl.SecurityManagement import getSecurityManager
from Acquisition import aq_base, aq_inner, aq_parent
from copy import deepcopy
from DateTime import DateTime
from DocumentTemplate.DT_Util import TemplateDict
from lxml import etree
from lxml.etree import Element, SubElement
from Products.CMFCore.Expression import Expression
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowException, ObjectDeleted,\
                                          ObjectMoved
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.DCWorkflow.Expression import StateChangeInfo
from Products.DCWorkflow.utils import Message as _
from Products.DCWorkflow.utils import modifyRolesForPermission
from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition as DCWorkflow
50
from Products.ERP5Type import Permissions, PropertySheet
51
from Products.ERP5Type.Cache import CachingMethod
52
from Products.ERP5Type.Globals import PersistentMapping
53 54 55 56 57 58 59 60
from Products.ERP5Type.id_as_reference import IdAsReferenceMixin
from Products.DCWorkflow.Expression import createExprContext
from Products.ERP5Type.patches.WorkflowTool import SECURITY_PARAMETER_ID,\
                                                          WORKLIST_METADATA_KEY
from Products.ERP5Type.Utils import UpperCase, convertToMixedCase
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5Workflow.Document.Transition import TRIGGER_AUTOMATIC,\
                                    TRIGGER_USER_ACTION, TRIGGER_WORKFLOW_METHOD
61
from tempfile import mktemp
62 63
from types import StringTypes
from zLOG import LOG, INFO, WARNING
64

65 66
ACTIVITY_GROUPING_COUNT = 100

67
class Workflow(IdAsReferenceMixin("", "prefix"), XMLObject):
68 69 70
  """
  A ERP5 Workflow.
  """
71
  id = ''
72 73
  meta_type = 'ERP5 Workflow'
  portal_type = 'Workflow'
74
  _isAWorkflow = True # DCWorkflow Tool compatibility
75 76 77
  add_permission = Permissions.AddPortalContent
  isPortalContent = 1
  isRADContent = 1
78
  default_reference = ''
79
  workflow_managed_permission = ()
80 81
  managed_role = ()
  manager_bypass = 0
82 83 84 85 86 87 88 89 90 91 92

  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = (
    PropertySheet.Base,
    PropertySheet.XMLObject,
    PropertySheet.CategoryCore,
    PropertySheet.DublinCore,
93
    PropertySheet.Reference,
94 95 96
    PropertySheet.Workflow,
  )

97
  def notifyCreated(self, document):
98

99 100 101 102 103 104 105
    """Notifies this workflow after an object has been created and added.
    """
    try:
        self._changeStateOf(document, None)
    except ( ObjectDeleted, ObjectMoved ):
        # Swallow.
        pass
106

107
  initializeDocument = notifyCreated
108 109 110 111 112

  def _generateHistoryKey(self):
    """
    Generate a key used in the workflow history.
    """
113 114
    history_key = self.getReference()
    return history_key
115 116 117 118 119 120 121 122 123 124

  def _updateWorkflowHistory(self, document, status_dict):
    """
    Change the state of the object.
    """
    # Create history attributes if needed
    if getattr(aq_base(document), 'workflow_history', None) is None:
      document.workflow_history = PersistentMapping()
      # XXX this _p_changed is apparently not necessary
      document._p_changed = 1
125

126 127 128 129
    # Add an entry for the workflow in the history
    workflow_key = self._generateHistoryKey()
    if not document.workflow_history.has_key(workflow_key):
      document.workflow_history[workflow_key] = ()
130

131
    # Update history
132
    document.workflow_history[workflow_key] += (status_dict,)
133 134
    # XXX this _p_changed marks the document modified, but the
    # only the PersistentMapping is modified
135
    # document._p_changed = 1
136
    # XXX this _p_changed is apparently not necessary
137
    #document.workflow_history._p_changed = 1
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157

  def getDateTime(self):
    """
    Return current date time.
    """
    return DateTime()

  def getStateChangeInformation(self, document, state, transition=None):
    """
    Return an object used for variable tales expression.
    """
    if transition is None:
      transition_url = None
    else:
      transition_url = transition.getRelativeUrl()
    return self.asContext(document=document,
                          transition=transition,
                          transition_url=transition_url,
                          state=state)

158 159 160 161 162 163 164 165 166 167 168
  def isWorkflowMethodSupported(self, document, transition_id):
    transition = self._getOb('transition_' + transition_id)
    sdef = self._getWorkflowStateOf(document, id_only=0)
    if sdef is None:
      return 0
    if (transition in sdef.getDestinationValueList() and
        self._checkTransitionGuard(transition, document) and
        transition.getTriggerType() == TRIGGER_WORKFLOW_METHOD
        ):
      return 1
    return 0
169

170 171 172 173 174 175 176 177 178
  security.declarePrivate('isActionSupported')
  def isActionSupported(self, document, action, **kw):
    '''
    Returns a true value if the given action name
    is possible in the current state.
    '''
    sdef = self._getWorkflowStateOf(document, id_only=0)
    if sdef is None:
      return 0
179

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
    if action in sdef.getDestinationIdList():
      tdef = self._getOb(action, None)
      if (tdef is not None and
        tdef.getTriggerType() == TRIGGER_USER_ACTION and
        self._checkTransitionGuard(tdef, document, **kw)):
        return 1
    return 0

  security.declarePrivate('isInfoSupported')
  def isInfoSupported(self, ob, name):
      '''
      Returns a true value if the given info name is supported.
      '''
      if name == self.getStateVariable():
          return 1
195
      vdef = self.getVariableValueDict().get(name, None)
196 197 198 199
      if vdef is None:
          return 0
      return 1

200 201
  def _checkTransitionGuard(self, transition, document, **kw):
    return transition.checkGuard(getSecurityManager(), self, document, **kw)
202 203 204 205 206 207 208 209 210 211 212 213

  def _findAutomaticTransition(self, document, sdef):
    tdef = None
    for t in sdef.getDestinationValueList():
      if t.getTriggerType() == TRIGGER_AUTOMATIC:
        if self._checkTransitionGuard(t, document):
          tdef = t
          break
    return tdef

  security.declarePrivate('updateRoleMappingsFor')
  def updateRoleMappingsFor(self, document):
214 215
    """
    Changes the object permissions according to the current state.
216 217
    """
    changed = 0
218 219 220 221 222
    state = self._getWorkflowStateOf(document, id_only=False)
    if state is not None:
      for permission, role_list in state.state_permission_roles.items():
        if modifyRolesForPermission(document, permission, role_list):
          changed = 1
223 224
    return changed

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 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 273 274 275 276 277
  # This method allows to update all objects using one workflow, for example
  # after the permissions per state for this workflow were modified
  def updateRoleMappings(self, REQUEST=None):
    """
    Changes permissions of all objects related to this workflow
    """
    # XXX(WORKFLOW) add test for roles update:
    #  - edit permission/roles on a workflow
    #  - check permission on an existing object of a type using this workflow
    workflow_tool = aq_parent(aq_inner(self))
    chain_by_type = workflow_tool._chains_by_type
    type_info_list = workflow_tool._listTypeInfo()
    workflow_id = self.id
    portal_type_id_list = []

    # look into old chain_by_type (for compatibility)
    for type_info in type_info_list:
      type_info_id = type_info.getId()
      if chain_by_type.has_key(type_info_id) and \
         workflow_id in chain_by_type[type_info_id]:
          portal_type_id_list.append(type_info_id)
      elif workflow_id in workflow_tool._default_chain:
        portal_type_id_list.append(type_info_id)

    # check the workflow defined on the portal type objects
    for portal_type in self.getPortalObject().portal_types.objectValues(portal_type='Base Type'):
      if workflow_id in portal_type.getTypeWorkflowList():
        portal_type_id_list.append(portal_type.getId())

    if portal_type_id_list:
      object_list = self.portal_catalog(portal_type=portal_type_id_list, limit=None)
      portal_activities = self.portal_activities
      object_path_list = [x.path for x in object_list]
      for i in xrange(0, len(object_list), ACTIVITY_GROUPING_COUNT):
        current_path_list = object_path_list[i:i+ACTIVITY_GROUPING_COUNT]
        portal_activities.activate(activity='SQLQueue',
                                    priority=3)\
              .callMethodOnObjectList(current_path_list,
                                      'updateRoleMappingsFor',
                                      wf_id = self.getId())
    else:
      object_list = []

    if REQUEST is not None:
      message = 'No object updated.'
      if object_list:
        message = '%d object(s) updated: \n %s.' % (len(object_list),
          ', '.join([o.getTitleOrId() + ' (' + o.getPortalType() + ')'
                     for o in object_list]))
      return message
    else:
      return len(object_list)

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
  def getManagedRoleList(self):
    return sorted(self.getPortalObject().getDefaultModule('acl_users').valid_roles())

  security.declarePrivate('doActionFor')
  def doActionFor(self, document, action, comment='', **kw):
    '''
    Allows the user to request a workflow action.  This method
    must perform its own security checks.
    '''
    sdef = self._getWorkflowStateOf(document, id_only=0)
    kw['comment'] = comment
    if sdef is None:
      raise WorkflowException(_(u'Object is in an undefined state.'))
    if self.isActionSupported(document, action, **kw):
      wf_id = self.getId()
      if wf_id is None:
        raise WorkflowException(
            _(u'Requested workflow not found.'))
    tdef = self._getOb(id=action)

    if tdef not in self.objectValues(portal_type='Transition'):
      raise Unauthorized(action)
    if tdef is None or tdef.getTriggerType() != TRIGGER_USER_ACTION:
      msg = _(u"Transition '${action_id}' is not triggered by a user "
        u"action.", mapping={'action_id': action})
      raise WorkflowException(msg)
    if not self._checkTransitionGuard(tdef, document, **kw):
      raise Unauthorized(action)
    self._changeStateOf(document, tdef, kw)

  def _changeStateOf(self, document, tdef=None, kwargs=None):
    '''
    Changes state.  Can execute multiple transitions if there are
    automatic transitions.  tdef set to None means the object
    was just created.
    '''
    moved_exc = None
    while 1:
      try:
        sdef = self._executeTransition(document, tdef, kwargs)
      except ObjectMoved, moved_exc:
        document = moved_exc.getNewObject()
        sdef = self._getWorkflowStateOf(document, id_only=0)
        # Re-raise after all transitions.
      if sdef is None:
        break
      tdef = self._findAutomaticTransition(document, sdef)
      if tdef is None:
        # No more automatic transitions.
        break
      # Else continue.
    if moved_exc is not None:
        # Re-raise.
      raise moved_exc

  def listObjectActions(self, info):
      fmt_data = None
      document = info.object
      sdef = self._getWorkflowStateOf(document, id_only=0)
      if sdef is None:
          return None
      res = []

      for tid in sdef.getDestinationIdList():
        tdef = self._getOb(id=tid)
        if tdef is not None and tdef.getTriggerType() == TRIGGER_USER_ACTION and \
344
                tdef.getActionName() and self._checkTransitionGuard(tdef, document):
345 346 347 348 349 350
            if fmt_data is None:
                fmt_data = TemplateDict()
                fmt_data._push(info)
            fmt_data._push({'transition_id': tdef.getReference()})
            res.append((tid, {
                'id': tdef.getReference(),
351 352 353
                'name': tdef.getActionName() % fmt_data,
                'url': str(tdef.getAction()) % fmt_data,
                'icon': str(tdef.getIcon()) % fmt_data,
354
                'permissions': (),  # Predetermined.
355
                'category': tdef.getActionType(),
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
                'transition': tdef}))
            fmt_data._pop()
      res.sort()

      return [ result[1] for result in res ]

  def getWorklistVariableMatchDict(self, info, check_guard=True):
    """
      Return a dict which has an entry per worklist definition
      (worklist id as key) and which value is a dict composed of
      variable matches.
    """
    if not self.objectValues(portal_type='Worklist'):
      return None

    portal = self.getPortalObject()
    def getPortalTypeListForWorkflow(workflow_id):
iv's avatar
iv committed
373
        portal_type_list = []
374 375 376
        for type_info in portal.portal_types.objectValues():
          portal_type = type_info.id
          if workflow_id in type_info.getTypeWorkflowList():
iv's avatar
iv committed
377 378
            portal_type_list.append(portal_type)
        return portal_type_list
379 380 381 382 383 384 385 386 387 388

    _getPortalTypeListForWorkflow = CachingMethod(getPortalTypeListForWorkflow,
                              id='_getPortalTypeListForWorkflow', cache_factory = 'erp5_ui_long')
    portal_type_list = _getPortalTypeListForWorkflow(self.id)
    if not portal_type_list:
      return None
    variable_match_dict = {}
    security_manager = getSecurityManager()
    workflow_id = self.getId()
    workflow_title = self.getTitle()
389 390
    for worklist_value in self.getWorklistValueList():
      action_box_name = worklist_value.getActionName()
391
      is_guarded = worklist_value.isGuarded()
392
      guard_role_list = worklist_value.getGuardRoleList()
393 394
      if action_box_name:
        variable_match = {}
395 396
        for key in worklist_value.getVarMatchKeys():
          var = worklist_value.getVarMatch(key)
397 398 399 400 401
          if isinstance(var, Expression):
            evaluated_value = var(createExprContext(StateChangeInfo(portal,
                                  self, kwargs=info.__dict__.copy())))
            if isinstance(evaluated_value, (str, int, long)):
              evaluated_value = [str(evaluated_value)]
402
          else:
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
            evaluated_value = [x % info for x in var]
          variable_match[key] = evaluated_value

        if 'portal_type' in variable_match and len(variable_match['portal_type']):
          portal_type_intersection = set(variable_match['portal_type']).intersection(portal_type_list)
          # in case the current workflow is not associated with portal_types
          # defined on the worklist, don't display the worklist for this
          # portal_type.
          variable_match['portal_type'] = list(portal_type_intersection)
        variable_match.setdefault('portal_type', portal_type_list)

        if len(variable_match.get('portal_type', [])) == 0:
          continue

        is_permitted_worklist = 0
418
        if not is_guarded:
419
          is_permitted_worklist = 1
420 421 422
        elif not check_guard or worklist_value.checkGuard(security_manager,
                                                          self, portal,
                                                          check_roles=False):
423
          is_permitted_worklist = 1
424
          variable_match[SECURITY_PARAMETER_ID] = guard_role_list
425 426 427 428 429 430 431 432

        if is_permitted_worklist:
          fmt_data = TemplateDict()
          fmt_data._push(info)
          variable_match.setdefault(SECURITY_PARAMETER_ID, ())
          fmt_data._push({k: ('&%s:list=' % k).join(v) for\
                                            k, v in variable_match.iteritems()})

433
          worklist_id = worklist_value.getReference()
434 435 436 437 438 439
          variable_match[WORKLIST_METADATA_KEY] = {
                                                'format_data': fmt_data,
                                                 'worklist_title': action_box_name,
                                                 'worklist_id': worklist_id,
                                                 'workflow_title': workflow_title,
                                                 'workflow_id': workflow_id,
440 441
                                                 'action_box_url': worklist_value.getAction(),
                                                 'action_box_category': worklist_value.getActionType()}
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456

          variable_match_dict[worklist_id] = variable_match

    if len(variable_match_dict) == 0:
      return None
    return variable_match_dict

  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.
      '''
      if name == self.getStateVariable():
          return self._getWorkflowStateOf(ob, 1)
457
      vdef = self.getVariableValueDict()[name]
458 459 460 461
      if vdef.getInfoGuard() is not None and not vdef.getInfoGuard().check(
          getSecurityManager(), self, ob):
          return default
      status = self.getCurrentStatusDict(ob)
462
      variable_expression = vdef.getVariableExpression()
463 464 465 466
      if status is not None and status.has_key(name):
          value = status[name]

      # Not set yet.  Use a default.
467
      elif variable_expression is not None:
468
          ec = createExprContext(StateChangeInfo(ob, self, status))
469
          value = Expression(variable_expression)(ec)
470
      else:
471
          value = vdef.getVariableValue()
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510

      return value

  def getCurrentStatusDict(self, document):
    """
    Get the current status dict. It's the same as _getStatusOf.
    """
    workflow_key = self._generateHistoryKey()
    workflow_history = self.getParent().getHistoryOf(workflow_key, document)
    # Copy is requested
    if workflow_history:
      return workflow_history[-1].copy()
    return {}

  def _getStatusOf(self, ob):
      tool = self.getParent()
      status = tool.getStatusOf(self.getId(), ob)
      if status is None:
          return {}
      else:
          # Copy is requested
          return status.copy()

  def _getWorkflowStateOf(self, ob, id_only=0):
      tool = self.getParent()
      id_no_suffix = self.getReference()
      status = tool.getStatusOf(id_no_suffix, ob)
      if status is None:
          state = self.getSourceValue()
      else:
          state_id = 'state_' + status.get(self.getStateVariable(), None)
          state = self._getOb(state_id)
          if state is None:
              state = self.getSourceValue()
      if id_only:
          return state.getReference()
      else:
          return state

511
  def getVariableValueDict(self):
512
    variable_dict = {}
513
    for vdef in self.objectValues(portal_type="Workflow Variable"):
514 515 516 517 518
      variable_dict[vdef.getReference()] = vdef
    return variable_dict

  def getVariableIdList(self):
    id_list = []
519
    for ob in self.objectValues(portal_type="Workflow Variable"):
520 521
      id_list.append(ob.getReference())
    return id_list
522

523
  def getStateValueById(self, stated_id):
524
    return self._getOb('state_' + stated_id, default=None)
525 526 527

  def getStateValueList(self):
    return self.objectValues(portal_type="State")
528

529 530
  def getStateIdList(self):
    id_list = []
531
    for ob in self.objectValues(portal_type="State"):
532 533
      id_list.append(ob.getReference())
    return id_list
534

535 536
  def getWorklistValueList(self):
    return self.objectValues(portal_type="Worklist")
537 538 539 540 541 542 543

  def getWorklistIdList():
    id_list = []
    for ob in self.objectValues(portal_type="Worklist"):
      id_list.append(ob.getReference())
    return id_list

544 545 546 547 548
  def getTransitionValueById(self, transition_reference):
    return self._getOb('transition_' + transition_reference, None)

  def getTransitionValueList(self):
    return self.objectValues(portal_type="Transition")
549 550

  def getTransitionIdList(self):
551 552
    return [ob.getReference() for ob
            in self.objectValues(portal_type="Transition")]
553

554 555
  def getScriptValueList(self):
    return self.objectValues(portal_type='Workflow Script')
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621

  def notifyWorkflowMethod(self, ob, transition_list, args=None, kw=None):
    """ Execute workflow methods.
    """
    if type(transition_list) in StringTypes:
      method_id = transition_list
    elif len(transition_list) == 1:
      method_id = transition_list[0]
    else:
      raise ValueError('WorkflowMethod should be attached to exactly 1 transition per DCWorkflow instance.')
    sdef = self._getWorkflowStateOf(ob)
    if sdef is None:
      raise WorkflowException, 'Object is in an undefined state'
    prefix_method_id = 'transition_' + method_id
    if prefix_method_id not in sdef.getDestinationIdList():
      raise Unauthorized(method_id)
    tdef = self._getOb(prefix_method_id)
    if tdef is None or tdef.getTriggerType() != TRIGGER_WORKFLOW_METHOD:
      raise WorkflowException, (
         'Transition %s is not triggered by a workflow method'
             % method_id)
    if not self._checkTransitionGuard(tdef, ob):
      raise Unauthorized(method_id)
    self._changeStateOf(ob, tdef, kw)
    if getattr(ob, 'reindexObject', None) is not None:
      if kw is not None:
        activate_kw = kw.get('activate_kw', {})
      else:
        activate_kw = {}
      ob.reindexObject(activate_kw=activate_kw)

  def notifyBefore(self, ob, transition_list, args=None, kw=None):
    pass

  def notifySuccess(self, ob, transition_list, result, args=None, kw=None):
    pass

  def notifyException(self, ob, action, exc):
      '''
      Notifies this workflow that an action failed.
      '''
      pass

  def _executeTransition(self, document, tdef=None, form_kw=None):
    """
    Execute transition.
    """
    sci = None
    econtext = None
    moved_exc = None
    validation_exc = None
    tool = getToolByName(self, 'portal_workflow')

    # Figure out the old and new states.
    state_var = self.getStateVariable()
    status_dict = self.getCurrentStatusDict(document)
    current_state_value = self._getWorkflowStateOf(document, id_only=0)

    if current_state_value == None:
      current_state_value = self.getSourceValue()
    old_state = current_state_value.getReference()
    old_sdef = current_state_value

    if tdef is None:
      new_sdef = self.getSourceValue()
      new_state = new_sdef.getReference()
622

623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
      if not new_sdef:
        # Do nothing if there is no initial state. We may want to create
        # workflows with no state at all, only for worklists.
        return
      former_status = {}
    else:
      new_sdef = tdef.getDestinationValue()
      if new_sdef == None:
        new_state = old_state
      else:
        new_state = new_sdef.getReference()
      former_status = self.getCurrentStatusDict(document)

    # Execute the "before" script.
    before_script_success = 1
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658

    if tdef is not None:
      script_value_list = tdef.getBeforeScriptValueList()
      if script_value_list:
        kwargs = form_kw
        sci = StateChangeInfo(document, self, former_status, tdef, old_sdef,
                              new_sdef, kwargs)
        for script in script_value_list:
          # Pass lots of info to the script in a single parameter.
          if script.getPortalType() != 'Workflow Script':
            raise NotImplementedError ('Unsupported Script %s for state %s' % 
                                       (script.id, old_sdef.getReference()))
          try:
            script(sci)  # May throw an exception.
          except ValidationFailed, validation_exc:
            before_script_success = 0
            before_script_error_message = deepcopy(validation_exc.msg)
            validation_exc_traceback = sys.exc_traceback
          except ObjectMoved, moved_exc:
            ob = moved_exc.getNewObject()
            # Re-raise after transition
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673

    # update variables
    state_values = None
    # seems state variable is not used in new workflow.
    object = self.getStateChangeInformation(document, self.getSourceValue())
    if new_sdef is not None:
      state_values = getattr(new_sdef,'var_values', None)
    if state_values is None:
      state_values = {}

    tdef_exprs = {}
    transition_variable_list = []
    if tdef is not None:
      transition_variable_list = tdef.objectValues(portal_type='Transition Variable')
    for transition_variable in transition_variable_list:
674
      tdef_exprs[transition_variable.getCausalityId()] = transition_variable.getVariableExpression()
675 676 677 678 679 680

    # Update all transition variables
    if form_kw is not None:
      object.REQUEST.other.update(form_kw)
      kwargs = form_kw

681
    for vdef in self.objectValues(portal_type='Workflow Variable'):
682 683
      id = vdef.getId()
      variable_reference = vdef.getReference()
684
      if not vdef.getStatusIncluded():
685 686 687 688 689 690 691 692 693 694
        continue
      expr = None
      if variable_reference in state_values:
        value = state_values[variable_reference]
      elif id in tdef_exprs:
        expr = tdef_exprs[id]
      elif not vdef.getAutomaticUpdate() and variable_reference in former_status:
        # Preserve former value
        value = former_status[variable_reference]
      else:
695 696
        if vdef.getVariableExpression() is not None:
          expr = vdef.getVariableExpression()
697
        else:
698
          value = vdef.getVariableValue(object=object)
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
      if expr is not None and expr != '':
        # Evaluate an expression.
        if econtext is None:
          # Lazily create the expression context.
          if sci is None:
            kwargs = form_kw
            sci = StateChangeInfo(
                document, self, former_status, tdef,
                old_sdef, new_sdef, kwargs)
          econtext = createExprContext(sci)
        expr = Expression(expr)
        value = expr(econtext)
      if value is None: value = ''
      status_dict[variable_reference] = value
    # Do not proceed in case of failure of before script
    if not before_script_success:
      status_dict[state_var] = old_state # Remain in state
      tool.setStatusOf(self.getReference(), document, status_dict)
      sci = StateChangeInfo(
        document, self, former_status, tdef, old_sdef, new_sdef, kwargs)
      # put the error message in the workflow history
      sci.setWorkflowVariable(error_message=before_script_error_message)
      if validation_exc :
        # reraise validation failed exception
        raise validation_exc, None, validation_exc_traceback
      return new_sdef

    # update state
    status_dict[state_var] = new_state
    object = self.getStateChangeInformation(document, current_state_value, transition=self)

    tool.setStatusOf(self.getReference(), document, status_dict)
    self.updateRoleMappingsFor(document)

    # Execute the "after" script.
734
    if tdef is not None:
735 736
      script_value_list = tdef.getAfterScriptValueList()
      if script_value_list:
737 738 739
        kwargs = form_kw
        sci = StateChangeInfo(
                  document, self, former_status, tdef, old_sdef, new_sdef, kwargs)
740
        for script in script_value_list:
741
          # Script can be either script or workflow method
742 743 744
          if script in old_sdef.getDestinationValueList() and \
              self._getOb(script.id).getTriggerType() == TRIGGER_WORKFLOW_METHOD:
            getattr(document, convertToMixedCase(self._getOb(script.id).getReference()))()
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
          else:
            # Pass lots of info to the script in a single parameter.
            if script.getPortalType() == 'Workflow Script':
              script(sci)  # May throw an exception.

    # Return the new state object.
    if moved_exc is not None:
        # Propagate the notification that the object has moved.
        raise moved_exc
    else:
        return new_sdef

  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.
    '''
    sdef = self._getWorkflowStateOf(ob)
    if sdef is None:
        raise WorkflowException, 'Object is in an undefined state'
    if method_id not in sdef.getTransitionIdList():
        raise Unauthorized(method_id)
767
    tdef = self.getTransitionValueById(method_id)
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
    if tdef is None or tdef.getTriggerType() != TRIGGER_WORKFLOW_METHOD:
        raise WorkflowException, (
            'Transition %s is not triggered by a workflow method'
            % method_id)
    if not self._checkTransitionGuard(tdef, ob):
        raise Unauthorized(method_id)
    res = func(*args, **kw)
    try:
        self._changeStateOf(ob, tdef, kw)
    except ObjectDeleted:
        # Re-raise with a different result.
        raise ObjectDeleted(res)
    except ObjectMoved, ex:
        # Re-raise with a different result.
        raise ObjectMoved(ex.getNewObject(), res)
    return res

  def addTransition(self, name):
    tr = self.newContent(portal_type='Transition')
    tr.setReference(name)

  def deleteTransitions(self, name_list):
    for name in name_list:
      self._delObject('transition_'+name)

  def showAsXML(self, root=None):
    if root is None:
      root = Element('erp5')
      return_as_object = False

    # Define a list of property to show to users:
    workflow_prop_id_to_show = ['description', 'state_var',
      'permissions', 'initial_state']

    # workflow as XML, need to rename DC workflow's portal_type before comparison.
    workflow = SubElement(root, 'workflow',
                        attrib=dict(reference=self.getReference(),
                        portal_type=self.getPortalType()))

    for prop_id in sorted(workflow_prop_id_to_show):
      # In most case, we should not synchronize acquired properties
      if prop_id not in ('uid', 'workflow_history', 'id', 'portal_type',):
        if prop_id == 'permissions':
811 812
          value = tuple(self.getProperty('workflow_managed_permission'))
          prop_type = self.getPropertyType('workflow_managed_permission')
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
        elif prop_id == 'initial_state':
          if self.getSourceValue() is not None:
            value = self.getSourceValue().getReference()
          else:
            value = ''
          prop_type = 'string'
        elif prop_id =='state_var':
          value = self.getProperty('state_variable')
          prop_type = self.getPropertyType('state_variable')
        else:
          value = self.getProperty(prop_id)
          prop_type = self.getPropertyType(prop_id)
        if value is None or value ==() or value == ():
          value = ''
        sub_object = SubElement(workflow, prop_id, attrib=dict(type=prop_type))
        sub_object.text = str(value)

    # 1. State as XML
    state_reference_list = []
    state_list = self.objectValues(portal_type='State')
    # show reference instead of id
    state_prop_id_to_show = ['description',
      'transitions', 'permission_roles']
    for sdef in state_list:
      state_reference_list.append(sdef.getReference())
    states = SubElement(workflow, 'states', attrib=dict(state_list=str(state_reference_list),
                        number_of_element=str(len(state_reference_list))))
    for sdef in state_list:
      state = SubElement(states, 'state', attrib=dict(reference=sdef.getReference(), portal_type=sdef.getPortalType()))
      for property_id in sorted(state_prop_id_to_show):
        if property_id == 'permission_roles':
          property_value = sdef.getProperty('state_permission_roles')
          property_type = sdef.getPropertyType('state_permission_roles')
        elif property_id == 'transitions':
          property_value = sdef.getDestinationIdList()
          destination_list = []
          for tr_id in property_value:
            destination_list.append(self._getOb(tr_id).getReference())
          property_value = tuple(destination_list)
          property_type = 'multiple selection'
        else:
          property_value = sdef.getProperty(property_id)
          property_type = sdef.getPropertyType(property_id)

        if property_value is None or property_value ==() or property_value == []:
          property_value = ''
        sub_object = SubElement(state, property_id, attrib=dict(type=property_type))
        sub_object.text = str(property_value)

    # 2. Transition as XML
    transition_reference_list = []
    transition_list = self.objectValues(portal_type='Transition')
    transition_prop_id_to_show = ['description', 'new_state_id',
866 867
      'trigger_type', 'script_name', 'after_script_name', 'action_type',
      'icon', 'action_name', 'action', 'roles', 'groups',
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882
      'permissions', 'expr', 'transition_variable']
    for tdef in self.objectValues(portal_type='Transition'):
      transition_reference_list.append(tdef.getReference())
    transitions = SubElement(workflow, 'transitions',
          attrib=dict(transition_list=str(transition_reference_list),
          number_of_element=str(len(transition_reference_list))))
    for tdef in transition_list:
      transition = SubElement(transitions, 'transition',
            attrib=dict(reference=tdef.getReference(),
            portal_type=tdef.getPortalType()))
      guard = SubElement(transition, 'guard', attrib=dict(type='object'))
      transition_variables = SubElement(transition, 'transition_variables', attrib=dict(type='object'))
      for property_id in sorted(transition_prop_id_to_show):
        if property_id in ('roles', 'groups', 'permissions', 'expr',):
          if property_id == 'roles':
883
            property_value = tdef.getGuardRoleList()
884
          if property_id == 'groups':
885
            property_value = tdef.getGuardGroupList()
886
          if property_id == 'permissions':
887
            property_value = tdef.getGuardPermissionList()
888
          if property_id == 'expr':
889
            property_value = tdef.getGuardExpression()
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920
          if property_value is None or property_value == [] or property_value == ():
            property_value = ''
          elif property_id != 'expr':
            property_value = tuple(property_value)
          sub_object = SubElement(guard, property_id, attrib=dict(type='guard configuration'))
        else:
          if property_id == 'new_state_id':
            if tdef.getDestinationValue() is not None:
              property_value = tdef.getDestinationValue().getReference()
            else:
              property_value = ''
            sub_object = SubElement(transition, property_id, attrib=dict(type='string'))
          elif property_id == 'script_name':
            property_value = tdef.getBeforeScriptIdList()
            if property_value == [] or property_value is None:
              property_value = ''
            else:
              property_value = self._getOb(tdef.getBeforeScriptIdList()[0]).getReference()
            sub_object = SubElement(transition, property_id, attrib=dict(type='string'))
          elif property_id == 'after_script_name':
            property_value = tdef.getAfterScriptIdList()
            if property_value == [] or property_value is None:
              property_value = ''
            else:
              property_value = self._getOb(tdef.getAfterScriptIdList()[0]).getReference()
            sub_object = SubElement(transition, property_id, attrib=dict(type='string'))
          elif property_id =='transition_variable':
            tr_var_list = tdef.objectValues(portal_type='Transition Variable')
            for tr_var in tr_var_list:
              reference = self._getOb(tr_var.getCausalityId()).getReference()
              transition_variable = SubElement(transition_variables, property_id, attrib=dict(id=reference,type='variable'))
921
              transition_variable.text = str(tr_var.getVariableExpression())
922 923 924 925 926 927 928 929 930 931
          else:
            property_value = tdef.getProperty(property_id)
            property_type = tdef.getPropertyType(property_id)
            sub_object = SubElement(transition, property_id, attrib=dict(type=property_type))
        if property_value is None or property_value ==() or property_value == []:
          property_value = ''
        sub_object.text = str(property_value)

    # 3. Variable as XML
    variable_reference_list = []
932
    variable_list = self.objectValues(portal_type='Workflow Variable')
933
    variable_prop_id_to_show = ['description', 'variable_expression',
iv's avatar
iv committed
934
          'for_catalog', 'for_status', 'automatic_update']
935 936 937
    for vdef in variable_list:
      variable_reference_list.append(vdef.getReference())
    variables = SubElement(workflow, 'variables', attrib=dict(variable_list=str(variable_reference_list),
938
                           number_of_element=str(len(variable_reference_list))))
939 940 941 942
    for vdef in variable_list:
      variable = SubElement(variables, 'variable', attrib=dict(reference=vdef.getReference(),
            portal_type=vdef.getPortalType()))
      for property_id in sorted(variable_prop_id_to_show):
943
        if property_id == 'automatic_update':
944 945
          property_value = vdef.getAutomaticUpdate()
          sub_object = SubElement(variable, property_id, attrib=dict(type='int'))
946 947 948 949
        elif property_id == 'variable_value':
          property_value = vdef.getVariableValue()
          if vdef.getVariableValue() is not None:
            property_value = vdef.getVariableValue()
950 951 952 953 954 955 956 957 958 959
          sub_object = SubElement(variable, property_id, attrib=dict(type='string'))
        else:
          property_value = vdef.getProperty(property_id)
          property_type = vdef.getPropertyType(property_id)
          sub_object = SubElement(variable, property_id, attrib=dict(type=property_type))
        if property_value is None or property_value ==() or property_value == []:
          property_value = ''
        sub_object.text = str(property_value)
        # for a very specific case, action return the reference of transition,
        # but in XML should show the same expression as in DC workflow.
960
        if vdef.getId() == 'variable_action' and property_id == 'variable_expression' and property_value != '':
961 962 963 964 965 966 967
          sub_object.text = str('transition/getId|nothing')

    # 4. Worklist as XML
    worklist_reference_list = []
    worklist_list = self.objectValues(portal_type='Worklist')
    worklist_prop_id_to_show = ['description', 'matched_portal_type_list',
          'matched_validation_state_list', 'matched_simulation_state_list',
968
          'action_type', 'action_name', 'action', 'icon',
969
          'roles', 'groups', 'permissions', 'expr']
970 971 972 973 974 975 976 977 978 979 980 981
    for qdef in worklist_list:
      worklist_reference_list.append(qdef.getReference())
    worklists = SubElement(workflow, 'worklists', attrib=dict(worklist_list=str(worklist_reference_list),
                        number_of_element=str(len(worklist_reference_list))))
    for qdef in worklist_list:
      worklist = SubElement(worklists, 'worklist', attrib=dict(reference=qdef.getReference(),
      portal_type=qdef.getPortalType()))
      guard = SubElement(worklist, 'guard', attrib=dict(type='object'))
      for property_id in sorted(worklist_prop_id_to_show):
         # show guard configuration:
        if property_id in ('roles', 'groups', 'permissions', 'expr',):
          if property_id == 'roles':
982
            property_value = qdef.getGuardRoleList()
983
          if property_id == 'groups':
984
            property_value = qdef.getGuardGroupList()
985
          if property_id == 'permissions':
986
            property_value = qdef.getGuardPermissionList()
987
          if property_id == 'expr':
988
            property_value = qdef.getGuardExpression()
989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075
          if property_value is not None:
            property_value = tuple(property_value)
          sub_object = SubElement(guard, property_id, attrib=dict(type='guard configuration'))
        else:
          property_value = qdef.getProperty(property_id)
          state_ref_list = []
          if property_id in ('matched_validation_state_list',
              'matched_simulation_state_list',) and property_value is not None:
            for sid in property_value:
              state_ref = self._getOb(sid).getReference()
              state_ref_list.append(state_ref)
            property_value = tuple(state_ref_list)
          if property_id == 'matched_portal_type_list':
            if property_value is not None:
              property_value = tuple(property_value)
          property_type = qdef.getPropertyType(property_id)
          sub_object = SubElement(worklist, property_id, attrib=dict(type=property_type))
        if property_value is None or property_value ==() or property_value == []:
          property_value = ''
        sub_object.text = str(property_value)

    # 5. Script as XML
    script_reference_list = []
    script_list = self.objectValues(portal_type='Workflow Script')
    script_prop_id_to_show = sorted(['body', 'parameter_signature','proxy_roles'])
    for sdef in script_list:
      script_reference_list.append(sdef.getReference())
    scripts = SubElement(workflow, 'scripts', attrib=dict(script_list=str(script_reference_list),
                        number_of_element=str(len(script_reference_list))))
    for sdef in script_list:
      script = SubElement(scripts, 'script', attrib=dict(reference=sdef.getReference(),
        portal_type=sdef.getPortalType()))
      for property_id in script_prop_id_to_show:
        if property_id == 'proxy_roles':
          property_value = tuple(sdef.getProperty('proxy_role_list'))
          property_type = sdef.getPropertyType('proxy_role_list')
        else:
          property_value = sdef.getProperty(property_id)
          property_type = sdef.getPropertyType(property_id)
        sub_object = SubElement(script, property_id, attrib=dict(type=property_type))
        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)

  # Get list of portal types for workflow
  def getPortalTypeListForWorkflow(self):
    """
      Get list of portal types for workflow.
    """
    result = []
    workflow_id = self.getId()
    for portal_type in self.getPortalObject().portal_types.objectValues():
      if workflow_id in portal_type.getTypeWorkflowList():
        result.append(portal_type.getId())
    return result

  def _executeMetaTransition(self, ob, new_state_id):
    """
    Allow jumping from state to another without triggering any hooks.
    Must be used only under certain conditions.
    """
    sci = None
    econtext = None
    tdef = None
    kwargs = None
    new_state_id_no_prefix = new_state_id
    new_state_id = 'state_' + new_state_id
    # Figure out the old and new states.
    old_sdef = self._getWorkflowStateOf(ob)
    if old_sdef is None:
      old_state = self._getWorkflowStateOf(ob, id_only=True)
    else:
      old_state = old_sdef.getId()
    if old_state == new_state_id:
      # Object is already in expected state
      return
    former_status = self.getCurrentStatusDict(ob)

    new_sdef = self._getOb(new_state_id, None)
    if new_sdef is None:
      raise WorkflowException, ('Destination state undefined: ' + new_state_id)

    # Update variables.
1076
    state_values = self.getVariableValueDict()
1077 1078 1079 1080 1081
    if state_values is None:
      state_values = {}

    tdef_exprs = {}
    status = {}
1082
    for id, vdef in self.getVariableValueDict().items():
1083
      if not vdef.getStatusIncluded():
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
        continue
      expr = None
      if state_values.has_key(id):
        value = state_values[id]
      elif tdef_exprs.has_key(id):
        expr = tdef_exprs[id]
      elif not vdef.getAutomaticUpdate() and former_status.has_key(id):
        # Preserve former value
        value = former_status[id]
      else:
1094 1095 1096
        variable_expression = vdef.getVariableExpression()
        if variable_expression is not None:
          expr = Expression(variable_expression)
1097
        else:
1098
          value = vdef.getVariableValue()
1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143
      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, old_sdef,
                                  new_sdef, kwargs)
          econtext = createExprContext(sci)
        value = expr(econtext)
      status[id] = value

    status['comment'] = 'Jump from %r to %r' % (self._getOb(old_state).getReference(), new_state_id_no_prefix,)
    status[self.getStateVariable()] = new_state_id_no_prefix
    tool = self.getParent()
    tool.setStatusOf(self.getId(), ob, status)

    # Update role to permission assignments.
    self.updateRoleMappingsFor(ob)
    return new_sdef

  security.declarePrivate('allowCreate')
  def allowCreate(self, container, type_name):
      """Returns true if the user is allowed to create a workflow instance.

      The object passed to the guard is the prospective container.

      wenjie: This is a compatibility related patch.

      More detail see TypeTool.pyline 360.
      """
      return 1

  def getCatalogVariablesFor(self, ob):
      '''
      Allows this workflow to make workflow-specific variables
      available to the catalog, making it possible to implement
      worklists in a simple way.
      Returns a mapping containing the catalog variables
      that apply to ob.
      '''
      initial_state = None
      res = {}
      # Always provide the state variable.
      state_var = self.getStateVariable()
      status = self.getCurrentStatusDict(ob)
1144
      for vdef_ref, vdef in self.getVariableValueDict().iteritems():
iv's avatar
iv committed
1145
          if vdef.getForCatalog():
1146
              variable_expression = vdef.getVariableExpression()
1147 1148 1149 1150
              if status.has_key(vdef_ref):
                  value = status[vdef_ref]

              # Not set yet.  Use a default.
1151
              elif variable_expression is not None:
1152 1153
                  ec = createExprContext(StateChangeInfo(ob, self, status))
                  # convert string to expression before execute it.
1154
                  value = Expression(variable_expression)(ec)
1155
              else:
1156
                  value = vdef.getVariableValue()
1157 1158 1159 1160 1161 1162
      if hasattr(self, 'getSourceValue'):
        if self.getSourceValue() is not None:
          initial_state = self.getSourceValue().getReference()
      if state_var is not None:
        res[state_var] = status.get(state_var, initial_state)
      return res
1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178

  def _setWorkflowManagedPermissionList(self, permission_list):
    self.workflow_managed_permission = permission_list

    # add/remove the added/removed workflow permission to each state
    for state in self.objectValues(portal_type='State'):
      state.setCellRange(sorted(permission_list),
                         sorted(self.getManagedRoleList()),
                         base_id='cell')
      for permission in permission_list:
        if permission not in state.state_permission_roles:
          state.state_permission_roles[permission] = []
      # remove permission from state_permission_roles dict
      for permission in state.state_permission_roles.keys():
        if permission not in permission_list:
          del state.state_permission_roles[permission]