Workflow.py 41 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
# 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.
#
##############################################################################
wenjie.zheng's avatar
wenjie.zheng committed
28 29

import os
30
import sys
31 32

from AccessControl import ClassSecurityInfo
33
from AccessControl.unauthorized import Unauthorized
wenjie.zheng's avatar
wenjie.zheng committed
34
from AccessControl.SecurityManagement import getSecurityManager
35
from Acquisition import aq_base, aq_inner, aq_parent
36
from copy import deepcopy
37
from DateTime import DateTime
wenjie.zheng's avatar
wenjie.zheng committed
38
from DocumentTemplate.DT_Util import TemplateDict
39 40
from lxml import etree
from lxml.etree import Element, SubElement
41 42
from Products.CMFCore.Expression import Expression
from Products.CMFCore.utils import getToolByName
wenjie.zheng's avatar
wenjie.zheng committed
43 44
from Products.CMFCore.WorkflowCore import WorkflowException, ObjectDeleted,\
                                          ObjectMoved
45
from Products.DCWorkflow.DCWorkflow import ValidationFailed
wenjie.zheng's avatar
wenjie.zheng committed
46 47 48 49 50 51 52 53
from Products.DCWorkflow.Expression import StateChangeInfo
from Products.DCWorkflowGraph.config import DOT_EXE
from Products.DCWorkflowGraph.DCWorkflowGraph import bin_search, getGraph
from Products.DCWorkflow.utils import Message as _
from Products.DCWorkflow.utils import modifyRolesForPermission
from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition as DCWorkflow
from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5Type.Globals import PersistentMapping
54
from Products.ERP5Type.id_as_reference import IdAsReferenceMixin
wenjie.zheng's avatar
wenjie.zheng committed
55 56 57 58 59 60 61 62 63
from Products.ERP5Type.patches.Expression import Expression_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
from tempfile import mktemp
from types import StringTypes
64 65
from xml.sax.saxutils import escape, unescape
from xml_marshaller.xml_marshaller import Marshaller
66
from zLOG import LOG, INFO, WARNING
67

68 69 70 71
MARSHALLER_NAMESPACE_URI = 'http://www.erp5.org/namespaces/marshaller'
marshaller = Marshaller(namespace_uri=MARSHALLER_NAMESPACE_URI,
                                                            as_tree=True).dumps

72
class Workflow(IdAsReferenceMixin("workflow_", "prefix"), XMLObject):
73 74 75 76 77 78
  """
  A ERP5 Workflow.
  """

  meta_type = 'ERP5 Workflow'
  portal_type = 'Workflow'
79
  _isAWorkflow = True # DCWorkflow Tool compatibility
80 81 82
  add_permission = Permissions.AddPortalContent
  isPortalContent = 1
  isRADContent = 1
83
  default_reference = ''
84 85 86 87 88
  managed_permission_list = ()
  managed_role = ()
  erp5_permission_roles = {} # { permission: [role] or (role,) }
  manager_bypass = 0

89 90 91 92 93 94 95 96 97 98
  # Declarative security
  security = ClassSecurityInfo()
  security.declareObjectProtected(Permissions.AccessContentsInformation)

  # Declarative properties
  property_sheets = (
    PropertySheet.Base,
    PropertySheet.XMLObject,
    PropertySheet.CategoryCore,
    PropertySheet.DublinCore,
99
    PropertySheet.Reference,
100 101 102
    PropertySheet.Workflow,
  )

wenjie.zheng's avatar
wenjie.zheng committed
103
  def notifyCreated(self, document):
104 105 106
    """
    Set initial state on the Document
    """
107
    state_var = self.getStateVariable()
108
    object = self.getStateChangeInformation(document, self.getSourceValue())
109

110
    # Initialize workflow history
111
    state_id = self.getSourceValue().getReference()
112
    status_dict = {state_var: state_id}
113
    variable_list = self.objectValues(portal_type='Variable')
114
    former_status = self._getOb(status_dict[state_var], None)
115 116
    ec = Expression_createExprContext(StateChangeInfo(document, self, former_status))

117
    for variable in variable_list:
118 119 120 121 122 123 124
      if variable.for_status == 0:
        continue
      if variable.default_expr is not None:
        expr = Expression(variable.default_expr)
        value = expr(ec)
      else:
        value = variable.getInitialValue(object=object)
125
      status_dict[variable.getReference()] = value
126

127
    self._updateWorkflowHistory(document, status_dict)
128
    self.updateRoleMappingsFor(document)
129

130 131
  initializeDocument = notifyCreated

132 133 134 135
  def _generateHistoryKey(self):
    """
    Generate a key used in the workflow history.
    """
136 137
    history_key = self.unrestrictedTraverse(self.getRelativeUrl()).getReference()
    return history_key
138 139 140 141 142 143 144 145 146 147

  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
148

149 150 151 152
    # 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] = ()
153

154
    # Update history
155
    document.workflow_history[workflow_key] += (status_dict,)
156 157
    # XXX this _p_changed marks the document modified, but the
    # only the PersistentMapping is modified
wenjie.zheng's avatar
wenjie.zheng committed
158
    # document._p_changed = 1
159
    # XXX this _p_changed is apparently not necessary
160 161
    #document.workflow_history._p_changed = 1

162 163 164 165 166
  def getCurrentStatusDict(self, document):
    """
    Get the current status dict.
    """
    workflow_key = self._generateHistoryKey()
167

168 169 170 171 172 173 174 175 176 177
    # Copy is requested
    result = document.workflow_history[workflow_key][-1].copy()
    return result

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

178 179 180
  def getManagedPermissionList(self):
    return self.managed_permission_list

181 182 183 184 185 186 187 188 189 190 191 192 193
  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)

wenjie.zheng's avatar
wenjie.zheng committed
194
  def isWorkflowMethodSupported(self, document, transition_id):
195
    transition = self._getOb('transition_' + transition_id)
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
    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.trigger_type == TRIGGER_WORKFLOW_METHOD
        ):
      return 1
    return 0

  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

216 217
    if action in sdef.getDestinationIdList():
      tdef = self._getOb(action, None)
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
      if (tdef is not None and
        tdef.trigger_type == 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
      vdef = self.objectValues(portal_type='Variable').get(name, None)
      if vdef is None:
          return 0
      return 1

  def _checkTransitionGuard(self, tdef, document, **kw):
    guard = tdef.getGuard()
    if guard is None:
      return 1
    if guard.check(getSecurityManager(), self, document, **kw):
      return 1
    return 0

  def _findAutomaticTransition(self, document, sdef):
    tdef = None
246 247
    for tid in sdef.getDestinationIdList():
      t = self._getOb(id=tid)
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
      if t is not None and t.trigger_type == TRIGGER_AUTOMATIC:
        if self._checkTransitionGuard(t, document):
          tdef = t
          break
    return tdef

  security.declarePrivate('updateRoleMappingsFor')
  def updateRoleMappingsFor(self, document):
    """Changes the object permissions according to the current state.
    """
    changed = 0
    sdef = self._getWorkflowStateOf(document, id_only=0)
    managed_permission = self.getManagedPermissionList()
    if sdef is None:
        return 0
wenjie.zheng's avatar
wenjie.zheng committed
263
    # zwj: get all matrix cell objects
264
    permission_role_matrix_cells = sdef.objectValues(portal_type = "PermissionRoles")
wenjie.zheng's avatar
wenjie.zheng committed
265
    # zwj: build a permission roles dict
266 267
    for perm_role in permission_role_matrix_cells:
      permission, role = perm_role.getPermissionRole()
wenjie.zheng's avatar
wenjie.zheng committed
268
      # zwj: double check the right role and permission are obtained
269 270 271 272 273
      if permission != 'None':
        if self.erp5_permission_roles.has_key(permission):
          self.erp5_permission_roles[permission] += (role,)
        else:
          self.erp5_permission_roles.update({permission : (role,)})
wenjie.zheng's avatar
wenjie.zheng committed
274
    # zwj: update role list to permission
275 276 277
    for permission_roles in self.erp5_permission_roles.keys():
      if modifyRolesForPermission(document, permission_roles, self.erp5_permission_roles[permission_roles]):
        changed = 1
wenjie.zheng's avatar
wenjie.zheng committed
278
        # zwj: clean Permission Role list for the next role mapping
279 280 281 282 283 284 285 286 287 288 289 290
      del self.erp5_permission_roles[permission_roles]
    return changed

  def getRoleList(self):
    return sorted(self.getPortalObject().getDefaultModule('acl_users').valid_roles())

  security.declarePrivate('doActionFor')
  def doActionFor(self, document, action, *args, **kw):
    sdef = self._getWorkflowStateOf(document, id_only=0)
    if sdef is None:
      raise WorkflowException(_(u'Object is in an undefined state.'))
    if self.isActionSupported(document, action, **kw):
291
      wf_id = self.getId()
292 293
      if wf_id is None:
        raise WorkflowException(
294 295
            _(u'Requested workflow not found.'))
    tdef = self._getOb(id=action)
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315

    if tdef not in self.objectValues(portal_type='Transition'):
      raise Unauthorized(action)
    if tdef is None or tdef.trigger_type != 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)

  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:
316
        sdef = self._executeTransition(document, tdef, kwargs)
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
      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 = []

340 341
      for tid in sdef.getDestinationIdList():
        tdef = self._getOb(id=tid)
342 343 344 345 346 347 348
        if tdef is not None and tdef.trigger_type == TRIGGER_USER_ACTION and \
                tdef.actbox_name and self._checkTransitionGuard(tdef, document):
            if fmt_data is None:
                fmt_data = TemplateDict()
                fmt_data._push(info)
            fmt_data._push({'transition_id': tid})
            res.append((tid, {
349
                'id': tdef.getReference(),
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
                'name': tdef.actbox_name % fmt_data,
                'url': str(tdef.actbox_url) % fmt_data,
                'icon': str(tdef.actbox_icon) % fmt_data,
                'permissions': (),  # Predetermined.
                'category': tdef.actbox_category,
                '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.
    """
367 368 369 370
    if not info.object.getPortalType() in ['Workflow', 'Interaction Workflow']:
      # avoid getting DC workflow
      return

371 372 373 374 375 376 377 378
    if not self.objectValues(portal_type='Worklist'):
      return None

    portal = self.getPortalObject()
    def getPortalTypeListForWorkflow(workflow_id):
        workflow_tool = portal.portal_workflow
        result = []
        append = result.append
379
        for workflow_id in info.object.getTypeInfo().getTypeWorkflowList():
380 381 382
            append(info.object.getTypeInfo().getId())
        return result

383
    portal_type_list = getPortalTypeListForWorkflow(self.id)
384 385 386 387
    if not portal_type_list:
      return None
    variable_match_dict = {}
    security_manager = getSecurityManager()
388
    workflow_id = self.id
389 390
    workflow_title = self.getTitle()
    for worklist_definition in self.objectValues(portal_type='Worklist'):
391
      worklist_id = worklist_definition.getId()
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
      action_box_name = worklist_definition.getActboxName()
      guard = worklist_definition.getGuard()
      if action_box_name:
        variable_match = {}
        for key in worklist_definition.getVarMatchKeys():
          var = worklist_definition.getVarMatch(key)
          if isinstance(var, Expression):
            evaluated_value = var(Expression_createExprContext(StateChangeInfo(portal,
                                  self, kwargs=info.__dict__.copy())))
            if isinstance(evaluated_value, (str, int, long)):
              evaluated_value = [str(evaluated_value)]
          else:
            evaluated_value = [str(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
        if guard is None:
          is_permitted_worklist = 1
        elif (not check_guard) or \
            Guard_checkWithoutRoles(guard, security_manager, self, portal):
          is_permitted_worklist = 1
          variable_match[SECURITY_PARAMETER_ID] = guard.roles

        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()})

          variable_match[WORKLIST_METADATA_KEY] = {
                                                'format_data': fmt_data,
                                                 'worklist_title': action_box_name,
                                                 'worklist_id': worklist_id,
                                                 'workflow_title': workflow_title,
438
                                                 'workflow_id': workflow_id,
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
                                                 'action_box_url': worklist_definition.actbox_url,
                                                 'action_box_category': worklist_definition.actbox_category}

          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.
      '''
      state_var = self.getStateVariable()
      if name == state_var:
456
          return ob._getDefaultAcquiredValue(state_var).getId()
457

458
      vdef = self._getOb(name)
459 460

      status_dict = self.getCurrentStatusDict(ob)
461
      former_status = self._getOb(status_dict[state_var], None)
462

463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
      if former_status == None:
        former_status = self.getSourceValue()

      if vdef.info_guard is not None and not vdef.info_guard.check(
          getSecurityManager(), self, ob):
          return default

      if status_dict is not None and name in status_dict:
          value = status_dict[name]
      # Not set yet.  Use a default.
      if vdef.default_expr is not None:
          ec = Expression_createExprContext(StateChangeInfo(ob, self, former_status))
          expr = Expression(vdef.default_expr)
          value = expr(ec)
      else:
          value = vdef.default_value
      return value

  def _getWorkflowStateOf(self, ob, id_only=0):
      tool = getToolByName(self, 'portal_workflow')
wenjie.zheng's avatar
wenjie.zheng committed
483
      id_no_suffix = self.getReference()
484
      status = tool.getStatusOf(id_no_suffix, ob)
485
      if status is None:
486
          state = self.getSourceValue()
487
      else:
488 489
          state_id = 'state_' + status.get(self.getStateVariable(), None)
          state = self._getOb(state_id)
490
          if state is None:
491
              state = self.getSourceValue()
492
      if id_only:
493
          return state.getReference()
494
      else:
495
          return state
496

497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
  def getVariableValueList(self):
    variable_dict = {}
    for vdef in self.objectValues(portal_type="Variable"):
      variable_dict[vdef.getReference()] = vdef
    return variable_dict

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

  def getStateValueList(self):
    state_dict = {}
    for sdef in self.objectValues(portal_type="State"):
      state_dict[sdef.getReference()] = sdef
    return state_dict

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

  def getWorklistValueList(self):
    worklist_dict = {}
    for qdef in self.objectValues(portal_type="Worklist"):
      worklist_dict[qdef.getReference()] = qdef
    return worklist_dict

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

  def getTransitionValueList(self):
    transition_dict = {}
    for tdef in self.objectValues(portal_type="Transition"):
      transition_dict[tdef.getReference()] = tdef
    return transition_dict

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

545 546 547 548 549 550
  def getScriptValueList(self):
    scripts = {}
    for script in self.objectValues(portal_type='Workflow Script'):
      scripts[script.getId()] = script
    return scripts

551 552 553 554 555 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
  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.trigger_type != 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 _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')
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 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
    # 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)
    status_dict['undo'] = 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()
      if not new_state:
        return
      former_status = {}
    else:
      new_sdef = tdef.getDestinationValue()
      if new_sdef == None:
        new_state = old_state
      else:
        new_state = new_sdef.getReference()
      former_status = current_state_value.getReference()

    # Execute the "before" script.
    before_script_success = 1
    if tdef is not None and tdef.getBeforeScriptId():
      script_id = tdef.getBeforeScriptId()
      if script_id:
        script = self._getOb(script_id)
        # Pass lots of info to the script in a single parameter.
        kwargs = form_kw
        sci = StateChangeInfo(
              document, self, former_status, tdef, old_sdef, new_sdef, kwargs)
        try:
          script.execute(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

    # update variables
    state_values = None
    if new_sdef is not None:
      state_values = getattr(new_sdef,'var_values', None)
    if state_values is None:
      state_values = {}

    if state_values is None: state_values = {}
    tdef_exprs = None
    if tdef is not None:
      tdef_exprs = tdef.objectValues(portal_type='Variable')
    if tdef_exprs is None: tdef_exprs = {}

    for vdef in self.objectValues(portal_type='Variable'):
      id = vdef.getId()
      id_no_suffix = vdef.getReference()
      if vdef.for_status == 0:
        continue
      expr = None
      if id_no_suffix in state_values:
        value = state_values[id_no_suffix]
      elif id in tdef_exprs:
        expr = tdef_exprs[id]
      elif not vdef.update_always and id in former_status:
        # 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:
            kwargs = form_kw
            sci = StateChangeInfo(
                document, self, former_status, tdef,
                old_sdef, new_sdef, kwargs)
          econtext = Expression_createExprContext(sci)
        expr = Expression(expr)
        value = expr(econtext)
      if id_no_suffix == "action":
        status_dict[id_no_suffix] = '_'.join(value.split('_')[1:])
      else:
        status_dict[id_no_suffix] = 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)
695
      # put the error message in the workflow history
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713
      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)
    # Update all transition variables
    if form_kw is not None:
      object.REQUEST.other.update(form_kw)

    for variable in self.objectValues(portal_type='Transition Variable'):
      status_dict[variable.getCausalityTitle()] = variable.getInitialValue(object=object)

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

715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
    # Execute the "after" script.
    script_id = getattr(tdef, 'getAfterScriptId')()
    if script_id is not None:
      kwargs = form_kw
      # Script can be either script or workflow method
      if script_id in old_sdef.getDestinationIdList() and \
          self._getOb(script_id).trigger_type == TRIGGER_WORKFLOW_METHOD:
        getattr(document, convertToMixedCase(self._getOb(script_id).getReference()))()
      else:
        script = self._getOb(script_id)
        # Pass lots of info to the script in a single parameter.
        if script.getTypeInfo().getId() == 'Workflow Script':
          sci = StateChangeInfo(
              document, self, former_status, tdef, old_sdef, new_sdef, kwargs)
          script.execute(sci)  # May throw an exception.
        else:
          raise NotImplementedError ('Unsupported Script %s for state %s'%(script_id, old_sdef.getReference()))
    # 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
738

739 740
  def showAsXML(self, root=None):
    if root is None:
741
      root = Element('erp5')
742 743
      return_as_object = False

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

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

    for prop_id in sorted(workflow_prop_id_to_show):
754 755
      # In most case, we should not synchronize acquired properties
      if prop_id not in ('uid', 'workflow_history', 'id', 'portal_type',):
756
        if prop_id == 'permissions':
757
          value = tuple(self.getProperty('workflow_managed_permission_list'))
758 759 760 761 762 763 764 765
          prop_type = self.getPropertyType('workflow_managed_permission_list')
          sub_object = SubElement(workflow, prop_id, attrib=dict(type=prop_type))
        elif prop_id == 'initial_state':
          value = self.getSourceValue().getReference()
          sub_object = SubElement(workflow, prop_id, attrib=dict(type='string'))
        elif prop_id =='state_var':
          value = self.getProperty('state_variable')
          sub_object = SubElement(workflow, prop_id, attrib=dict(type='string'))
766
        else:
767 768 769 770 771 772 773
          value = self.getProperty(prop_id)
          if value is None:
            # not registered if not defined.
            continue
          else:
            prop_type = self.getPropertyType(prop_id)
          sub_object = SubElement(workflow, prop_id, attrib=dict(type=prop_type))
774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
        if prop_type in ('object',):
          # We may have very long lines, so we should split
          value = aq_base(value)
          value = dumps(value)
          sub_object.text = standard_b64encode(value)
        elif prop_type in ('data',):
          # Create blocks to represent data
          # <data><block>ZERD</block><block>OEJJM</block></data>
          size_block = 60
          if isinstance(value, str):
            for index in xrange(0, len(value), size_block):
              content = value[index:index + size_block]
              data_encoded = standard_b64encode(content)
              block = SubElement(sub_object, 'block_data')
              block.text = data_encoded
          else:
            raise ValueError("XMLExportImport failed, the data is undefined")
        elif prop_type in ('lines', 'tokens',):
792 793 794 795 796 797 798
          if prop_id == 'initial_state':
            if self.getSourceValue():
              sub_object.text = self.getSourceValue().getReference()
          else:
            value = [word.decode('utf-8').encode('ascii','xmlcharrefreplace')\
                for word in value]
            sub_object.append(marshaller(value))
799 800 801 802 803 804 805
        elif prop_type in ('text', 'string',):
          if type(value) in (tuple, list, dict):
            sub_object.text = str(value)
          else:
            sub_object.text = unicode(escape(value), 'utf-8')
        elif prop_type != 'None':
          sub_object.text = str(value)
806
    """
807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828
    # We should now describe security settings
    for user_role in self.get_local_roles():
      local_role_node = SubElement(workflow, 'local_role',
                                   attrib=dict(id=user_role[0], type='tokens'))
      #convert local_roles in string because marshaller can't do it
      role_list = []
      for role in user_role[1]:
        if isinstance(role, unicode):
          role = role.encode('utf-8')
        role_list.append(role)
      local_role_node.append(marshaller(tuple(role_list)))
    if getattr(self, 'get_local_permissions', None) is not None:
      for user_permission in self.get_local_permissions():
        local_permission_node = SubElement(workflow, 'local_permission',
                                attrib=dict(id=user_permission[0], type='tokens'))
        local_permission_node.append(marshaller(user_permission[1]))
    # Sometimes theres is roles specified for groups, like with CPS
    if getattr(self, 'get_local_group_roles', None) is not None:
      for group_role in self.get_local_group_roles():
        local_group_node = SubElement(workflow, 'local_group',
                                      attrib=dict(id=group_role[0], type='tokens'))
        local_group_node.append(marshaller(group_role[1]))
829
    """
830 831 832
    # 1. State as XML
    state_reference_list = []
    state_list = self.objectValues(portal_type='State')
833
    # show reference instead of id
834 835
    state_prop_id_to_show = ['title', 'description',
      'transitions', 'permission_roles']
836 837 838 839 840
    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:
841
      state = SubElement(states, 'state', attrib=dict(reference=sdef.getReference(), portal_type=sdef.getPortalType()))
842
      for property_id in sorted(state_prop_id_to_show):
843 844 845 846 847 848 849 850
        if property_id == 'permission_roles':
          property_value = sdef.getProperty('state_permission_roles')
          property_type = sdef.getPropertyType('state_permission_roles')
          sub_object = SubElement(state, property_id, attrib=dict(type='string'))
        elif property_id == 'transitions':
          property_value = sdef.getDestinationValueList()
          property_type = sdef.getPropertyType('categories_list')
          sub_object = SubElement(state, property_id, attrib=dict(type='multiple selection'))
851
        else:
852 853 854 855 856 857 858 859
          property_value = sdef.getProperty(property_id)
          if property_value is None:
            # do not register if not defined.
            continue
          else:
            property_type = sdef.getPropertyType(property_id)
          sub_object = SubElement(state, property_id, attrib=dict(type=property_type))

860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877
        if property_type in ('object',):
          # We may have very long lines, so we should split
          property_value = aq_base(property_value)
          property_value = dumps(property_value)
          sub_object.text = standard_b64encode(property_value)
        elif property_type in ('data',):
          # Create blocks to represent data
          # <data><block>ZERD</block><block>OEJJM</block></data>
          size_block = 60
          if isinstance(property_value, str):
            for index in xrange(0, len(property_value), size_block):
              content = property_value[index:index + size_block]
              data_encoded = standard_b64encode(content)
              block = SubElement(sub_object, 'block_data')
              block.text = data_encoded
          else:
            raise ValueError("XMLExportImport failed, the data is undefined")
        elif property_type in ('lines', 'tokens',):
878 879 880 881 882 883 884 885 886 887
          if property_id == 'transitions':
            value_list = property_value
            value_reference_list = []
            for value in value_list:
              value_reference_list.append(value.getReference())
            sub_object.text = str(tuple(value_reference_list))
          else:
            property_value = [word.decode('utf-8').encode('ascii','xmlcharrefreplace')\
                for word in property_value]
            sub_object.append(marshaller(property_value))
888 889 890 891 892 893 894
        elif property_type in ('text', 'string',):
          if type(property_value) in (tuple, list, dict):
            sub_object.text = str(property_value)
          else:
            sub_object.text = unicode(escape(property_value), 'utf-8')
        elif property_type != 'None':
          sub_object.text = str(property_value)
895

896 897 898
    # 2. Transition as XML
    transition_reference_list = []
    transition_list = self.objectValues(portal_type='Transition')
899 900 901 902
    transition_prop_id_to_show = ['title', 'description', 'new_state_id',
      'trigger_type', 'script_name', 'after_script_name', 'actbox_category',
      'actbox_icon', 'actbox_name', 'actbox_url', 'role_list', 'group_list',
      'permission_list', 'expression']
903 904
    for tdef in self.objectValues(portal_type='Transition'):
      transition_reference_list.append(tdef.getReference())
905 906 907
    transitions = SubElement(workflow, 'transitions',
          attrib=dict(transition_list=str(transition_reference_list),
          number_of_element=str(len(transition_reference_list))))
908
    for tdef in transition_list:
909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948
      transition = SubElement(transitions, 'transition',
            attrib=dict(reference=tdef.getReference(),
            portal_type=tdef.getPortalType()))
      guard = SubElement(transition, 'guard', attrib=dict(type='string'))
      for property_id in sorted(transition_prop_id_to_show):
        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'))
        # show guard configuration:
        elif property_id in ('role_list', 'group_list', 'permission_list',
              'expression',):
          property_value = tdef.getProperty(property_id)
          if property_value is None or property_value == []:
            property_value = ''
          sub_object = SubElement(guard, property_id, attrib=dict(type='guard configuration'))
        else:
          property_value = tdef.getProperty(property_id)
          if property_value is None:
            property_value = ''
          else:
            property_type = tdef.getPropertyType(property_id)
          sub_object = SubElement(transition, property_id, attrib=dict(type=property_type))
        sub_object.text = str(property_value)
949

950
    """
951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979
    # 3. Variable as XML
    variable_reference_list = []
    variable_list = self.objectValues(portal_type='Variable')
    for vdef in variable_list:
      variable_reference_list.append(vdef.getReference())
    variables = SubElement(workflow, 'variables', attrib=dict(variable_list=str(variable_reference_list),
                        number_of_element=str(len(variable_reference_list))))
    for vdef in variable_list:
      variable = SubElement(variables, 'variable', attrib=dict(reference=vdef.getReference()))

    # 4. Worklist as XML
    worklist_reference_list = []
    worklist_list = self.objectValues(portal_type='Worklist')
    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()))

    # 5. Script as XML
    script_reference_list = []
    script_list = self.objectValues(portal_type='Workflow Script')
    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()))
980
    """
981

982
    # return xml object
983 984 985 986 987
    if return_as_object:
      return root
    return etree.tostring(root, encoding='utf-8',
                          xml_declaration=True, pretty_print=True)

988 989


990 991
  ###########
  ## Graph ##
992
  ###########
993

994 995 996
  getGraph = getGraph

  def getPOT(self, *args, **kwargs):
997
      """
998 999 1000 1001 1002 1003 1004 1005
      get the pot, copy from:
      "dcworkfow2dot.py":http://awkly.org/Members/sidnei/weblog_storage/blog_27014
      and Sidnei da Silva owns the copyright of the this function
      """
      out = []
      transition_dict = {}
      out.append('digraph "%s" {' % self.getTitle())
      transition_with_init_state_list = []
1006
      for state in self.objectValues(portal_type='State'):
1007 1008
        out.append('%s [shape=box,label="%s",' \
                     'style="filled",fillcolor="#ffcc99"];' % \
1009
                     (state.getId(), state.getTitle()))
1010 1011
        # XXX Use API instead of getDestinationValueList
        for available_transition in state.getDestinationValueList():
1012
          transition_with_init_state_list.append(available_transition.getId())
1013 1014 1015 1016
          destination_state = available_transition.getDestinationValue()
          if destination_state is None:
            # take care of 'remain in state' transitions
            destination_state = state
1017
          #
1018
          key = (state.getId(), destination_state.getId())
1019 1020 1021 1022 1023
          value = transition_dict.get(key, [])
          value.append(available_transition.getTitle())
          transition_dict[key] = value

      # iterate also on transitions, and add transitions with no initial state
1024
      for transition in self.objectValues(portal_type='Transition'):
1025
        trans_id = transition.getId()
1026 1027 1028 1029 1030
        if trans_id not in transition_with_init_state_list:
          destination_state = transition.getDestinationValue()
          if destination_state is None:
            dest_state_id = None
          else:
1031
            dest_state_id = destination_state.getId()
1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043

          key = (None, dest_state_id)
          value = transition_dict.get(key, [])
          value.append(transition.getTitle())
          transition_dict[key] = value

      for k, v in transition_dict.items():
          out.append('%s -> %s [label="%s"];' % (k[0], k[1],
                                                 ',\\n'.join(v)))

      out.append('}')
      return '\n'.join(out)