Commit df85ef46 authored by Arnaud Fontaine's avatar Arnaud Fontaine

ERP5Workflow: DC Workflows are now ERP5 objects (!1378).

This also moves all Configurator Workflows in workflow_module to portal_workflow
(workflow_module was an implementation of Workflows based on ERP5 objects and
not using DCWorkflow code).

* Workflows are now defined on on portal_workflow._chains_by_type anymore but,
  as everything else, on the Portal Type itself.
* portal_workflow can contain and work at the same time with legacy and new
  Workflows (ERP5Type/patches/DCWorkflow.py monkey-patching DCWorkflow classes
  to provide the same API).
* Existing Workflow Scripts should work as they are and the code can be updated
  later on to take advantage of the new API:
  + With legacy implementation Workflow {Scripts,Transitions,Worklists,States}
    were in a Folder ({scripts,transitions,worklists,states} attribute) but
    all of these are now in the Workflow itself and their IDs are prefixed
    (PropertySheet-style), for example `script_`. Legacy attributes are
    provided in new implementation to call the new API.
  + When calling a Workflow Script, `container` was bound to its parent, namely
    WF.scripts (Folder) and a Workflow Script could call another. Now `container`
    is bound to the WF itself and Workflow Scripts are in a Workflow directly.
    New implementation `scripts` attribute handle such use case.
  + Override portal_workflow.__getattr__ so that a Workflow Script can call
    another one without prefix.
* Worklist are Predicate: Worklist filter objects based on given criterions and
  thus it makes more sense for a Worklist to be a Predicate (albeit a Predicate
  with only Identity Criterion and nothing else).
  + Criterion Properties:
    * state_variable.
    * local_roles (SECURITY_PARAMETER_ID).
    * Any Workflow Variables with for_catalog == 1.

erp5_performance_test:testWorkflowPerformance were ran to compare DCWorkflow
and ERP5Workflow implementations and it seems to be about 4% slower with the
new implementation (legacy: 7.547, 7.593, 7.618, 7.59, 7.514 and new: 7.842,
7.723, 7.902, 7.837, 7.875).

Work done by Wenjie Zheng, Isabelle Vallet, Sebastien Robin and myself.
parent 9c73f05b
......@@ -35,7 +35,7 @@ from Products.CMFCore.utils import _checkPermission
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import reindex
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from AccessControl.SecurityManagement import newSecurityManager
from Products.ERP5Type.tests.Sequence import SequenceList
from Products.ERP5Form.PreferenceTool import Priority
......
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.ERP5Type.Message import translateString
closing_period = state_change['object']
......
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.ERP5Type.Message import translateString
period = state_change['object']
......
......@@ -2,7 +2,7 @@
XXX why proxy role ???
"""
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.ERP5Type.Message import translateString
transaction = state_change['object']
......
......@@ -3,7 +3,7 @@
XXX why proxy role ???
"""
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.ERP5Type.Message import translateString
transaction = state_change['object']
......
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.ERP5Type.Message import translateString
internal_invoice = state_change['object']
......
......@@ -23,7 +23,6 @@ def dumpWorkflowChain(self, ignore_default=False,
if ignore_id_set is None:
ignore_id_set = set()
workflow_tool = self.getPortalObject().portal_workflow
cbt = workflow_tool._chains_by_type
ti = workflow_tool._listTypeInfo()
types_info = []
for t in ti:
......@@ -32,15 +31,14 @@ def dumpWorkflowChain(self, ignore_default=False,
if title == id_:
title = None
chain = None
if cbt is not None and cbt.has_key(id_):
cbt_list = [x for x in cbt[id_] if not(x in ignore_id_set)]
if keep_order:
chain = cbt_list
else:
chain = sorted(cbt_list)
else:
cbt_list = [x for x in t.getTypeWorkflowList() if not(x in ignore_id_set)]
if not cbt_list:
if not(ignore_default):
chain = ['(Default)']
elif keep_order:
chain = cbt_list
else:
chain = sorted(cbt_list)
if chain:
types_info.append({'id': id_,
'title': title,
......
from Products.ERP5Type.Message import Message
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
inventory = state_change['object']
......
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.ERP5Type.Message import Message
# Check new catalog or catalog is the same as previous archive
......
......@@ -32,7 +32,7 @@ import unittest
from DateTime import DateTime
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5ReportTestCase
from erp5.component.test.testAccounting import AccountingTestCase
......
......@@ -35,7 +35,7 @@ from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
from erp5.component.mixin.EncryptedPasswordMixin import EncryptedPasswordMixin
from erp5.component.mixin.LoginAccountProviderMixin import LoginAccountProviderMixin
from erp5.component.mixin.ERP5UserMixin import ERP5UserMixin
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
from Products.CMFCore.utils import _checkPermission
from Products.CMFCore.exceptions import AccessControl_Unauthorized
......
......@@ -5,7 +5,7 @@
# In this case we want to be sure that open assignments share the same site category.
# XXX
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
# Get the assignment object and its parent
assignment_object = state_change['object']
......
# XXX: Duplicates Base_checkConsistency so proxy role is really effective.
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Core.Workflow import ValidationFailed
message_list = [x.getTranslatedMessage() for x in state_change['object'].checkConsistency()]
if message_list:
raise ValidationFailed(message_list)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_view</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_view</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>configurator_settings</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Action Information</string> </value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>10.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Configurator</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/WorkflowTransition_viewConfigurator</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
def titleToReference(title):
reference = title.replace(' ', '_').lower()
return reference
def migrateToERP5Workflow(portal_workflow, configurator_workflow):
"""
Convert Configurator Workflow (workflow_module/*) to ERP5 Workflow.
"""
workflow_id = configurator_workflow.getId()
relative_url = "%s/%s" % (portal_workflow.getRelativeUrl(), workflow_id)
def getCategoryList(prefix, category_value_list):
return ['%s/%s%s' % (relative_url, prefix, titleToReference(o.getTitle()))
for o in category_value_list ]
workflow = portal_workflow.newContent(
portal_type='Workflow',
reference=configurator_workflow.getId(),
comment=configurator_workflow.getComment(),
description=configurator_workflow.getDescription(),
state_base_category=configurator_workflow.getProperty('state_base_category'),
state_variable=configurator_workflow.getProperty('state_variable_name'),
source_list=getCategoryList('state_', configurator_workflow.getSourceValueList()),
# ConfiguratorWorkflow PropertySheet
configuration_after_script_id=configurator_workflow.getConfigurationAfterScriptId())
for business_configuration in configurator_workflow.getRelatedValueList(
portal_type='Business Configuration'):
business_configuration.setResourceValue(workflow)
for subobject in configurator_workflow.objectValues():
title = subobject.getTitle()
reference = titleToReference(title)
if subobject.getPortalType() == 'State':
state = workflow.newContent(
portal_type='State',
reference=reference,
title=title,
destination_list=getCategoryList('transition_', subobject.getDestinationValueList()),
comment=subobject.getComment(),
description=subobject.getDescription())
for business_configuration in subobject.getRelatedValueList(
portal_type='Business Configuration'):
business_configuration.setCurrentStateValue(state)
elif subobject.getPortalType() == 'Workflow Transition':
# XXX_1: Workflows only call Workflow Script and do not call Python Script in
# portal_skins but Configurator Workflows do. For now leave them as they
# ({before,after}_script_id property) but they should be migrated later on.
#
# def addWorkflowScript(script_id_property):
# old_script_id = getattr(subobject.aq_base, script_id_property, None)
# if old_script_id:
# old_script = portal_workflow.getPortalObject().unrestrictedTraverse(old_script_id)
# script = workflow.newContent(id=workflow.getScriptIdByReference(old_script.id),
# portal_type='Workflow Script')
# script.defeault_reference = old_script.id
# script.setTitle(old_script.title)
# script.setParameterSignature('state_change')
# script.setBody(old_script._body)
# script.setProxyRole(old_script._proxy_roles)
# return script
# before_script_value = addWorkflowScript('before_script_id')
# after_script_value = addWorkflowScript('after_script_id')
transition = workflow.newContent(
portal_type='Workflow Transition',
reference=reference,
title=title,
destination_list=getCategoryList('state_', subobject.getDestinationValueList()),
comment=subobject.getComment(),
description=subobject.getDescription(),
# XXX_1: before_script_value=before_script_value,
# after_script_value=after_script_value,
guard_expression=subobject.getProperty('guard_expression'),
# ConfiguratorWorkflowTransition Property Sheet
transition_form_id=subobject.getProperty('transition_form_id'))
# XXX_1: Should use the normal {before,after}_script Workflow Property.
try:
transition.before_script_id = subobject.aq_base.before_script_id
except AttributeError:
pass
try:
transition.after_script_id = subobject.aq_base.after_script_id
except AttributeError:
pass
# XXX: Transition Variable: Not used in erp5.git, used elsewhere?
elif subobject.getPortalType() == 'Variable':
if reference in ('action',
'actor',
'comment',
'error_message',
'history',
'portal_type',
'time'):
continue
workflow.newContent(
portal_type='Workflow Variable',
reference=reference,
title=title,
description=subobject.getDescription(),
automatic_update=subobject.getAutomaticUpdate(),
variable_default_expression=subobject.aq_base.initial_value,
comment=subobject.getComment())
# default_image
elif subobject.getPortalType() == 'Embedded File':
copy_data = configurator_workflow.manage_copyObjects([subobject.getId()])
workflow.manage_pasteObjects(copy_data)
else:
raise NotImplementedError(
"%s: Portal Type '%s'" % (subobject.getRelativeUrl(),
subobject.getPortalType()))
return workflow
def migrateWorkflowModuleToPortalWorkflow(self, recreate=False):
"""
Migrate workflow_module to new-style ERP5Workflows: workflow_module was implemented
for Configurators, based on DCWorkflow (portal_workflow), and a step towards migrating
DCWorkflows to ERP5 Objects. Now that portal_workflow is a real ERP5 Object, these must
be migrated to portal_workflow.
"""
portal = self.getPortalObject()
try:
workflow_module = portal.workflow_module
except AttributeError:
return "Nothing to do as workflow_module does not exist."
portal_workflow = portal.portal_workflow
assert portal_workflow.getPortalType() == 'Workflow Tool'
from zLOG import LOG
for configurator_workflow in workflow_module.objectValues():
id_ = configurator_workflow.getId()
if id_ in portal_workflow:
if not recreate:
continue
portal_workflow._delOb(id_)
new_workflow = migrateToERP5Workflow(portal_workflow, configurator_workflow)
LOG("migrateWorkflowModuleToPortalWorkflow", 0,
"Migrated %s to %s" % (configurator_workflow.getRelativeUrl(),
new_workflow.getRelativeUrl()))
return 'Done'
\ No newline at end of file
......@@ -14,7 +14,7 @@
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>ERP5UpgraderUtils</string> </value>
<value> <string>migrateWorkflowModuleToPortalWorkflow</string> </value>
</item>
<item>
<key> <string>description</string> </key>
......@@ -24,7 +24,7 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>extension.erp5.ERP5UpgraderUtils</string> </value>
<value> <string>extension.erp5.migrateWorkflowModuleToPortalWorkflow</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
......