wf: add external workflow action (#40204)
This commit is contained in:
parent
c55fba5f78
commit
017b204bcd
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue