wf: add external workflow action (#40204)

This commit is contained in:
Serghei Mihai 2020-02-28 11:29:57 +01:00
parent c55fba5f78
commit 017b204bcd
6 changed files with 403 additions and 4 deletions

View File

@ -1245,7 +1245,7 @@ def test_form_delete_field_existing_data(pub):
resp = resp.forms[0].submit()
resp = resp.follow()
assert len(FormDef.get(1).fields) == 0
def test_form_duplicate_field(pub):
user = create_superuser(pub)
create_role()
@ -3536,6 +3536,52 @@ def test_workflows_global_actions_timeout_triggers(pub):
assert Workflow.get(workflow.id).global_actions[0].triggers[0].timeout == '-2'
def test_workflows_global_actions_external_workflow_action(pub):
create_superuser(pub)
Workflow.wipe()
wf = Workflow(name='external')
action = wf.add_global_action('Global action')
trigger = action.append_trigger('webservice')
trigger.identifier = 'test'
item = action.append_item('remove')
wf.store()
formdef = FormDef()
formdef.name = 'external'
formdef.workflow = wf
formdef.store()
workflow = Workflow(name='foo')
st = workflow.add_status('New')
workflow.store()
app = login(get_app(pub))
resp = app.get('/backoffice/workflows/%s/status/%s/' % (workflow.id, st.id))
assert 'External workflow' not in [o[0] for o in resp.forms[0]['action-formdata-action'].options]
# activate option
if not pub.site_options.has_section('options'):
pub.site_options.add_section('options')
pub.site_options.set('options', 'external-workflow', 'true')
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
pub.site_options.write(fd)
resp = app.get('/backoffice/workflows/%s/status/%s/' % (workflow.id, st.id))
resp.forms[0]['action-formdata-action'] = 'External workflow'
resp = resp.forms[0].submit().follow()
assert 'External workflow (not completed)' in resp.text
resp = app.get('/backoffice/workflows/%s/status/%s/items/1/' % (workflow.id, st.id))
resp = resp.forms[0].submit('submit')
assert "required field" in resp.text
resp.forms[0]['slug'] = 'formdef:%s' % formdef.url_name
resp = resp.forms[0].submit('submit')
assert "required field" in resp.text
resp = resp.forms[0].submit('submit')
resp.forms[0]['trigger_id'] = 'action:%s' % trigger.identifier
resp = resp.forms[0].submit('submit').follow().follow()
def test_workflows_criticality_levels(pub):
create_superuser(pub)
create_role()

View File

@ -22,6 +22,7 @@ from wcs.wf.profile import UpdateUserProfileStatusItem
from wcs.wf.backoffice_fields import SetBackofficeFieldsWorkflowStatusItem
from wcs.wf.redirect_to_url import RedirectToUrlWorkflowStatusItem
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
from wcs.wf.external_workflow import ExternalWorkflowGlobalAction
from wcs.roles import Role
from wcs.fields import StringField, FileField
@ -755,3 +756,39 @@ def test_create_formdata(pub):
wf.store()
assert_import_export_works(wf, include_id=True)
def test_external_workflow(pub):
target_wf = Workflow(name='External global action')
action = target_wf.add_global_action('Delete', 'delete')
trigger = action.append_trigger('webservice')
trigger.trigger_id = 'Cleanup'
target_wf.store()
target_formdef = FormDef()
target_formdef.name = 'target form'
target_formdef.workflow = target_wf
target_formdef.store()
wf = Workflow(name='External workflow call')
st1 = wf.add_status('New')
st2 = wf.add_status('Call external workflow')
jump = ChoiceWorkflowStatusItem()
jump.id = '_external'
jump.label = 'Cleanup'
jump.by = ['_submitter']
jump.status = st2.id
jump.parent = st1
st1.items.append(jump)
external_workflow = ExternalWorkflowGlobalAction()
external_workflow.id = '_external_workflow'
external_workflow.slug = 'formdef:%s' % target_formdef.url_name
external_workflow.event = trigger.id
external_workflow.parent = st2
st2.items.append(external_workflow)
wf.store()
assert_import_export_works(wf, include_id=True)

View File

@ -34,7 +34,8 @@ from wcs.workflows import (Workflow, WorkflowStatusItem,
CommentableWorkflowStatusItem, ChoiceWorkflowStatusItem,
DisplayMessageWorkflowStatusItem,
AbortActionException, WorkflowCriticalityLevel,
AttachmentEvolutionPart, WorkflowBackofficeFieldsFormDef)
AttachmentEvolutionPart, WorkflowBackofficeFieldsFormDef,
perform_items)
from wcs.wf.aggregation_email import (AggregationEmailWorkflowStatusItem,
AggregationEmail, send_aggregation_emails)
from wcs.wf.anonymise import AnonymiseWorkflowStatusItem
@ -55,6 +56,7 @@ from wcs.wf.redirect_to_url import RedirectToUrlWorkflowStatusItem
from wcs.wf.notification import SendNotificationWorkflowStatusItem
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
from wcs.wf.external_workflow import ExternalWorkflowGlobalAction
from utilities import (create_temporary_pub, MockSubstitutionVariables,
@ -4664,3 +4666,160 @@ def test_create_carddata(pub):
assert carddef.data_class().count() == 0
formdata.perform_workflow()
assert carddef.data_class().count() == 0
def test_call_external_workflow_with_evolution_linked_object(pub):
FormDef.wipe()
CardDef.wipe()
LoggedError.wipe()
external_wf = Workflow(name='External Workflow')
st1 = external_wf.add_status(name='New')
action = external_wf.add_global_action('Delete', 'delete')
action.append_item('remove')
trigger = action.append_trigger('webservice')
trigger.identifier = 'delete'
external_wf.store()
external_formdef = FormDef()
external_formdef.name = 'External Form'
external_formdef.fields = [
StringField(id='0', label='string', varname='form_string'),
]
external_formdef.workflow = external_wf
external_formdef.store()
external_carddef = CardDef()
external_carddef.name = 'External Card'
external_carddef.fields = [
StringField(id='0', label='string', varname='card_string'),
]
external_carddef.workflow = external_wf
external_carddef.store()
wf = Workflow(name='External actions')
st1 = wf.add_status('Create external formdata')
create_formdata = CreateFormdataWorkflowStatusItem()
create_formdata.label = 'create linked form'
create_formdata.formdef_slug = external_formdef.url_name
create_formdata.varname = 'created_form'
create_formdata.id = '_create_form'
mappings = [
Mapping(field_id='0', expression='{{ form_var_string }}')
]
create_formdata.mappings = mappings
create_formdata.parent = st1
create_carddata = CreateCarddataWorkflowStatusItem()
create_carddata.label = 'create linked card'
create_carddata.formdef_slug = external_carddef.url_name
create_carddata.varname = 'created_card'
create_carddata.id = '_create_card'
create_carddata.mappings = mappings
create_carddata.parent = st1
st1.items.append(create_formdata)
st1.items.append(create_carddata)
global_action = wf.add_global_action('Delete external linked object', 'delete')
action = global_action.append_item('external_workflow_global_action')
action.slug = 'formdef:%s' % external_formdef.url_name
action.trigger_id = 'action:%s' % trigger.identifier
wf.store()
formdef = FormDef()
formdef.name = 'External action form'
formdef.fields = [
StringField(id='0', label='string', varname='string'),
]
formdef.workflow = wf
formdef.store()
assert external_formdef.data_class().count() == 0
assert external_carddef.data_class().count() == 0
formdata = formdef.data_class()()
formdata.data = {'0': 'test form'}
formdata.store()
formdata.just_created()
formdata.perform_workflow()
assert external_formdef.data_class().count() == 1
assert external_carddef.data_class().count() == 1
external_formdata = external_formdef.data_class().select()[0]
perform_items([action], formdata)
assert LoggedError.count() == 0
assert external_formdef.data_class().count() == 0
assert external_carddef.data_class().count() == 1
perform_items([action], formdata)
assert LoggedError.count() == 1
logged_error = LoggedError.select()[0]
assert logged_error.summary == 'Could not find linked "External Form" object by id %s' % external_formdata.id
assert logged_error.exception_class == 'KeyError'
def test_call_external_workflow_with_data_sourced_object(pub):
FormDef.wipe()
CardDef.wipe()
LoggedError.wipe()
carddef_wf = Workflow(name='Carddef Workflow')
st1 = carddef_wf.add_status(name='New')
action = carddef_wf.add_global_action('Delete', 'delete')
action.append_item('remove')
trigger = action.append_trigger('webservice')
trigger.identifier = 'delete'
carddef_wf.store()
carddef = CardDef()
carddef.name = 'Data'
carddef.fields = [
StringField(id='0', label='string', varname='card_string'),
]
carddef.digest_template = '{{ form_var_card_string }}'
carddef.workflow = carddef_wf
carddef.store()
carddata = carddef.data_class()()
carddata.data = {'0': 'Text'}
carddata.store()
wf = Workflow(name='External actions')
st1 = wf.add_status('Action')
global_action = wf.add_global_action('Delete external linked object', 'delete')
action = global_action.append_item('external_workflow_global_action')
action.slug = 'carddef:%s' % carddef.url_name
action.trigger_id = 'action:%s' % trigger.identifier
wf.store()
datasource = {'type': 'carddef:%s' % carddef.url_name}
formdef = FormDef()
formdef.name = 'External action form'
formdef.fields = [
ItemField(id='0', label='Card',
type='item', varname='card',
data_source=datasource)
]
formdef.workflow = wf
formdef.store()
assert LoggedError.count() == 0
assert carddef.data_class().count() == 1
formdata = formdef.data_class()()
formdata.data = {'0': '1'}
formdata.store()
formdata.just_created()
formdata.perform_workflow()
perform_items([action], formdata)
assert LoggedError.count() == 0
assert carddef.data_class().count() == 0
perform_items([action], formdata)
assert LoggedError.count() == 1
logged_error = LoggedError.select()[0]
assert logged_error.summary == 'Could not find linked "Data" object by id %s' % carddata.id
assert logged_error.exception_class == 'KeyError'

View File

@ -22,7 +22,6 @@ from .qommon import _
from wcs.carddata import CardData
from wcs.formdef import FormDef
from wcs.workflows import Workflow
if not hasattr(types, 'ClassType'):
types.ClassType = type
@ -80,7 +79,8 @@ class CardDef(FormDef):
@classmethod
def get_default_workflow(cls):
from wcs.workflows import EditableWorkflowStatusItem, ChoiceWorkflowStatusItem
from wcs.workflows import (EditableWorkflowStatusItem,
ChoiceWorkflowStatusItem, Workflow)
from wcs.wf.remove import RemoveWorkflowStatusItem
workflow = Workflow(name=_('Default (cards)'))
workflow.id = '_carddef_default'

152
wcs/wf/external_workflow.py Normal file
View File

@ -0,0 +1,152 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2020 Entr'ouvert
#
# 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, see <http://www.gnu.org/licenses/>.
from quixote import get_publisher
from wcs.qommon import _
from wcs.qommon.form import SingleSelectWidget
from wcs.logged_errors import LoggedError
from wcs.workflows import WorkflowStatusItem, perform_items, register_item_class
from wcs.workflows import WorkflowGlobalActionWebserviceTrigger, Workflow
from wcs.wf.create_formdata import LinkedFormdataEvolutionPart
from wcs.carddef import CardDef
from wcs.formdef import FormDef
class ExternalWorkflowGlobalAction(WorkflowStatusItem):
description = _('External workflow')
key = 'external_workflow_global_action'
category = 'formdata-action'
slug = None
trigger_id = None
@classmethod
def is_available(cls, workflow=None):
return get_publisher().has_site_option('external-workflow')
def get_workflow_webservice_triggers(self, workflow):
for action in workflow.global_actions or []:
for trigger in action.triggers or []:
if isinstance(trigger, WorkflowGlobalActionWebserviceTrigger):
yield trigger
def get_object_def(self, object_slug=None):
slug = object_slug or self.slug
object_type, slug = slug.split(':')
if object_type == 'formdef':
object_class = FormDef
elif object_type == 'carddef':
object_class = CardDef
try:
return object_class.get_by_urlname(slug)
except KeyError:
pass
def get_trigger(self, workflow):
try:
trigger_type, trigger_id = self.trigger_id.split(':', 1)
except ValueError:
return
for trigger in self.get_workflow_webservice_triggers(workflow):
if trigger.identifier == trigger_id:
return trigger
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
super(ExternalWorkflowGlobalAction, self).add_parameters_widgets(
form, parameters, prefix=prefix, formdef=formdef)
if 'slug' in parameters:
objects = [(None, '---', '')]
for wf in Workflow.select():
if any(self.get_workflow_webservice_triggers(wf)):
for objectdef in wf.formdefs(lightweight=True) + wf.carddefs(lightweight=True):
object_slug = '%s:%s' % (objectdef.__class__.__name__.lower(),
objectdef.url_name)
objects.append((object_slug, objectdef.name, object_slug))
if len(objects) == 1:
form.add_global_errors([_('No workflow with external triggerable global action ')])
return
objects.sort(key=lambda x: x[1])
form.add(SingleSelectWidget, '%sslug' % prefix,
title=_('Form/Card'),
value=self.slug,
required=True,
options=objects)
if 'trigger_id' in parameters and form.get('%sslug' % prefix):
object_def = self.get_object_def(form.get('%sslug' % prefix))
triggers_names = [(None, '---', '')]
for trigger in self.get_workflow_webservice_triggers(object_def.workflow):
if trigger.identifier:
trigger_id = 'action:%s' % trigger.identifier
triggers_names.append((trigger_id, trigger.parent.name, trigger_id))
form.add(SingleSelectWidget, '%strigger_id' % prefix,
title=_('Action'),
value=self.trigger_id,
required=True,
options=triggers_names)
def get_line_details(self):
if self.slug and self.trigger_id:
objectdef = self.get_object_def()
trigger = self.get_trigger(objectdef.workflow)
return _('action "%(trigger_name)s" on %(object_name)s') % {
'trigger_name': trigger.parent.name,
'object_name': objectdef.name}
return _('not completed')
def iter_target_datas(self, formdata, objectdef):
data_ids = []
# search linked objects in data sources
for field in formdata.get_formdef().get_all_fields():
if field.data_source and field.data_source['type'] == self.slug:
data_ids.append(formdata.data.get(field.id))
# search in evolution
for part in formdata.iter_evolution_parts():
if isinstance(part, LinkedFormdataEvolutionPart) and part.formdef_class == objectdef.__class__:
data_ids.append(part.formdata_id)
for target_id in data_ids:
try:
yield objectdef.data_class().get(target_id)
except KeyError as e:
# use custom error message depending on target type
LoggedError.record(_('Could not find linked "%(object_name)s" object by id %(object_id)s') % {
'object_name': objectdef.name, 'object_id': target_id},
formdata=formdata, exception=e)
def get_parameters(self):
return ('slug', 'trigger_id', 'condition')
def perform(self, formdata):
objectdef = self.get_object_def()
if not objectdef:
return
trigger = self.get_trigger(objectdef.workflow)
if not trigger:
LoggedError.record(_('No trigger with id "%s" found in workflow') % self.trigger_id)
return
for target_data in self.iter_target_datas(formdata, objectdef):
perform_items(trigger.parent.items, target_data)
register_item_class(ExternalWorkflowGlobalAction)

View File

@ -44,6 +44,7 @@ from .conditions import Condition
from .roles import Role, logged_users_role, get_user_roles
from .fields import FileField
from .formdef import FormDef
from .carddef import CardDef
from .formdata import Evolution
from .mail_templates import MailTemplate
@ -848,6 +849,9 @@ class Workflow(StorableObject):
def formdefs(self, **kwargs):
return list(FormDef.select(lambda x: x.workflow_id == self.id, **kwargs))
def carddefs(self, **kwargs):
return list(CardDef.select(lambda x: x.workflow_id == self.id, **kwargs))
class XmlSerialisable(object):
node_name = None
@ -2992,5 +2996,6 @@ def load_extra():
from .wf import notification
from .wf import create_formdata
from .wf import create_carddata
from .wf import external_workflow
from .wf.export_to_model import ExportToModel