wcs/wcs/wf/external_workflow.py

240 lines
8.9 KiB
Python

# 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.carddef import CardDef
from wcs.formdef import FormDef
from wcs.qommon import _
from wcs.qommon.form import ComputedExpressionWidget, Form, RadiobuttonsWidget, SingleSelectWidget
from wcs.workflows import (
Workflow,
WorkflowGlobalActionWebserviceTrigger,
WorkflowStatusItem,
perform_items,
register_item_class,
)
class ExternalWorkflowGlobalAction(WorkflowStatusItem):
description = _('External workflow')
key = 'external_workflow_global_action'
category = 'formdata-action'
automatic_targetting = _('Action on linked cards/forms')
manual_targetting = _('Specify the identifier of the card/form on which the action will be applied')
slug = None
target_mode = None
target_id = 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) and trigger.identifier:
yield trigger
def get_object_def(self, object_slug=None):
slug = object_slug or self.slug
try:
object_type, slug = slug.split(':')
except (AttributeError, ValueError):
return None
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_id = self.trigger_id.split(':', 1)[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, **kwargs):
super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
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 'target_mode' in parameters:
target_modes = [
('all', self.automatic_targetting, 'all'),
('manual', self.manual_targetting, 'manual'),
]
form.add(
RadiobuttonsWidget,
'%starget_mode' % prefix,
title=_('Targeting'),
value=self.target_mode or 'all',
required=True,
options=target_modes,
attrs={'data-dynamic-display-parent': 'true'},
)
if 'target_id' in parameters:
form.add(
ComputedExpressionWidget,
'%starget_id' % prefix,
value=self.target_id,
required=False,
attrs={
'data-dynamic-display-child-of': 'target_mode',
'data-dynamic-display-value': 'manual',
},
)
if 'trigger_id' in parameters and form.get('%sslug' % prefix):
object_def = self.get_object_def(form.get('%sslug' % prefix))
if not object_def:
return
triggers_names = [(None, '---', '')]
for trigger in self.get_workflow_webservice_triggers(object_def.workflow):
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,
)
if kwargs.get('orig') == 'variable_widget':
return
errors = [w.name for w in form.get_all_widgets() if w.has_error()]
if set(errors) == set(['%strigger_id' % prefix]):
form.ERROR_NOTICE = _('This action is configured in two steps. See below for details.')
else:
form.ERROR_NOTICE = Form.ERROR_NOTICE
def get_line_details(self):
if self.slug and self.trigger_id:
objectdef = self.get_object_def()
if objectdef:
trigger = self.get_trigger(objectdef.workflow)
if trigger:
return _('action "%(trigger_name)s" on %(object_name)s') % {
'trigger_name': trigger.parent.name,
'object_name': objectdef.name,
}
return _('not completed')
def get_manual_target(self, formdata):
if self.target_mode != 'manual':
return
objectdef = self.get_object_def()
target_id = self.compute(self.target_id, formdata=formdata, status_item=self)
if not target_id:
return
try:
return objectdef.data_class().get(target_id)
except KeyError as e:
# use custom error message depending on target type
get_publisher().record_error(
_('Could not find targeted "%(object_name)s" object by id %(object_id)s')
% {'object_name': objectdef.name, 'object_id': target_id},
formdata=formdata,
status_item=self,
exception=e,
)
def iter_target_datas(self, formdata, objectdef):
if self.target_mode == 'manual':
# return only target
target = self.get_manual_target(formdata)
if target:
yield target
return
yield from formdata.iter_target_datas(objectdef=objectdef, object_type=self.slug, status_item=self)
def get_parameters(self):
return ('slug', 'trigger_id', 'target_mode', 'target_id', 'condition')
def perform(self, formdata):
objectdef = self.get_object_def()
if not objectdef:
return
trigger = self.get_trigger(objectdef.workflow)
if not trigger:
get_publisher().record_error(
_('No trigger with id "%s" found in workflow') % self.trigger_id,
formdata=formdata,
status_item=self,
)
return
class CallerSource:
def __init__(self, formdata):
self.formdata = formdata
def get_substitution_variables(self):
return {'caller_form': self.formdata.get_substitution_variables(minimal=True)['form']}
caller_source = CallerSource(formdata)
formdata.store()
for target_data in self.iter_target_datas(formdata, objectdef):
with get_publisher().substitutions.temporary_feed(target_data):
get_publisher().substitutions.reset()
get_publisher().substitutions.feed(get_publisher())
get_publisher().substitutions.feed(target_data.formdef)
get_publisher().substitutions.feed(target_data)
get_publisher().substitutions.feed(caller_source)
perform_items(trigger.parent.items, target_data)
# update local object as it may have been modified by target_data
# workflow executions.
formdata.refresh_from_storage()
register_item_class(ExternalWorkflowGlobalAction)