workflows: add possibility to trigger global actions with a webservice (#32184)
This commit is contained in:
parent
2513ad6665
commit
c57cc62ac9
|
@ -64,6 +64,23 @@ workflow du formulaire.
|
|||
<output>{"url": null, "err": 0}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Il est également possible de définir des déclencheurs au niveau des actions
|
||||
globales du workflow, ils pourront alors être appelés quel que soit le statut
|
||||
de la demande.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Un tel appel, avec un déclencheur global ici intitulé <code>urgent</code> se
|
||||
ferait ainsi :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" -X POST \
|
||||
https://www.example.net/api/forms/newsletter/14/hooks/urgent<var>?signature…</var></input>
|
||||
<output>{"err": 0}</output>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
</page>
|
||||
|
|
|
@ -28,6 +28,7 @@ from wcs.categories import Category
|
|||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.workflows import Workflow, EditableWorkflowStatusItem, WorkflowBackofficeFieldsFormDef
|
||||
from wcs.wf.jump import JumpWorkflowStatusItem
|
||||
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
|
||||
from wcs import fields, qommon
|
||||
from wcs.api_utils import sign_url, get_secret_and_orig, is_url_signed, DEFAULT_DURATION
|
||||
from wcs.qommon.errors import AccessForbiddenError
|
||||
|
@ -2277,6 +2278,75 @@ def test_workflow_trigger_jump_once(pub, local_user):
|
|||
assert resp.json == {'err': 0, 'url': None}
|
||||
assert formdef.data_class().get(formdata.id).status == 'wf-st3'
|
||||
|
||||
|
||||
def test_workflow_global_webservice_trigger(pub, local_user):
|
||||
workflow = Workflow(name='test')
|
||||
st1 = workflow.add_status('Status1', 'st1')
|
||||
|
||||
ac1 = workflow.add_global_action('Action', 'ac1')
|
||||
trigger = ac1.append_trigger('webservice')
|
||||
trigger.identifier = 'plop'
|
||||
|
||||
add_to_journal = RegisterCommenterWorkflowStatusItem()
|
||||
add_to_journal.id = '_add_to_journal'
|
||||
add_to_journal.comment = 'HELLO WORLD'
|
||||
ac1.items.append(add_to_journal)
|
||||
add_to_journal.parent = ac1
|
||||
|
||||
workflow.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.fields = []
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdef.data_class().wipe()
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
assert formdef.data_class().get(formdata.id).status == 'wf-st1'
|
||||
|
||||
# call to undefined hook
|
||||
resp = get_app(pub).post(sign_uri(formdata.get_url() + 'hooks/XXX/'), status=404)
|
||||
resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/XXX/'), status=404)
|
||||
|
||||
# anonymous call
|
||||
resp = get_app(pub).post(formdata.get_url() + 'hooks/plop/', status=200)
|
||||
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD'
|
||||
|
||||
add_to_journal.comment = 'HELLO WORLD 2'
|
||||
workflow.store()
|
||||
resp = get_app(pub).post(formdata.get_api_url() + 'hooks/plop/', status=200)
|
||||
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 2'
|
||||
|
||||
# call requiring user
|
||||
add_to_journal.comment = 'HELLO WORLD 3'
|
||||
trigger.roles = ['logged-users']
|
||||
workflow.store()
|
||||
resp = get_app(pub).post(formdata.get_api_url() + 'hooks/plop/', status=403)
|
||||
resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/'), status=200)
|
||||
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 3'
|
||||
|
||||
# call requiring roles
|
||||
add_to_journal.comment = 'HELLO WORLD 4'
|
||||
trigger.roles = ['logged-users']
|
||||
workflow.store()
|
||||
Role.wipe()
|
||||
role = Role(name='xxx')
|
||||
role.store()
|
||||
trigger.roles = [role.id]
|
||||
workflow.store()
|
||||
resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/'), status=403)
|
||||
resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/', user=local_user), status=403)
|
||||
|
||||
local_user.roles = [role.id]
|
||||
local_user.store()
|
||||
resp = get_app(pub).post(sign_uri(formdata.get_api_url() + 'hooks/plop/', user=local_user), status=200)
|
||||
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == 'HELLO WORLD 4'
|
||||
|
||||
|
||||
def test_tracking_code(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
|
|
|
@ -593,6 +593,15 @@ def test_global_timeout_trigger(pub):
|
|||
assert wf2.global_actions[0].triggers[-1].id == trigger.id
|
||||
assert wf2.global_actions[0].triggers[-1].anchor == trigger.anchor
|
||||
|
||||
def test_global_webservice_trigger(pub):
|
||||
wf = Workflow(name='global actions')
|
||||
ac1 = wf.add_global_action('Action', 'ac1')
|
||||
trigger = ac1.append_trigger('webservice')
|
||||
trigger.identifier = 'plop'
|
||||
|
||||
wf2 = assert_import_export_works(wf, include_id=True)
|
||||
assert wf2.global_actions[0].triggers[-1].id == trigger.id
|
||||
assert wf2.global_actions[0].triggers[-1].identifier == trigger.identifier
|
||||
|
||||
def test_profile_action(pub):
|
||||
wf = Workflow(name='status')
|
||||
|
|
|
@ -1272,6 +1272,7 @@ class GlobalActionPage(WorkflowStatusPage):
|
|||
available_triggers = [
|
||||
('timeout', _('Automatic')),
|
||||
('manual', _('Manual')),
|
||||
('webservice', _('Webservice')),
|
||||
]
|
||||
form.add(SingleSelectWidget, 'type', title=_('Type'),
|
||||
required=True, options=available_triggers)
|
||||
|
|
16
wcs/api.py
16
wcs/api.py
|
@ -189,15 +189,25 @@ class ApiFormPage(BackofficeFormPage):
|
|||
if component == 'ics':
|
||||
return self.ics()
|
||||
|
||||
# check access for all paths, to block access to formdata that would
|
||||
# otherwise be accessible if the user is the submitter.
|
||||
self.check_access()
|
||||
# check access for all paths (except webooks), to block access to
|
||||
# formdata that would otherwise be accessible if the user is the
|
||||
# submitter.
|
||||
if not self.is_webhook:
|
||||
self.check_access()
|
||||
try:
|
||||
formdata = self.formdef.data_class().get(component)
|
||||
except KeyError:
|
||||
raise TraversalError()
|
||||
return ApiFormdataPage(self.formdef, formdata)
|
||||
|
||||
def _q_traverse(self, path):
|
||||
self.is_webhook = False
|
||||
if len(path) > 1:
|
||||
# webhooks have their own access checks, request cannot be blocked
|
||||
# at this point.
|
||||
self.is_webhook = bool(path[1] == 'hooks')
|
||||
return super(ApiFormPage, self)._q_traverse(path)
|
||||
|
||||
|
||||
class ApiFormsDirectory(Directory):
|
||||
_q_exports = ['', 'geojson']
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2019 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/>.
|
||||
|
||||
import json
|
||||
|
||||
from quixote import get_request, get_response
|
||||
from quixote.directory import Directory
|
||||
|
||||
from qommon import errors
|
||||
|
||||
from wcs.api import get_user_from_api_query_string, is_url_signed
|
||||
from wcs.roles import logged_users_role
|
||||
from wcs.workflows import (
|
||||
WorkflowGlobalActionWebserviceTrigger,
|
||||
get_role_translation,
|
||||
perform_items)
|
||||
|
||||
|
||||
class HookDirectory(Directory):
|
||||
_q_exports = ['']
|
||||
|
||||
def __init__(self, formdata, action, trigger):
|
||||
self.formdata = formdata
|
||||
self.action = action
|
||||
self.trigger = trigger
|
||||
|
||||
def _q_index(self):
|
||||
get_response().set_content_type('application/json')
|
||||
|
||||
if not get_request().get_method() == 'POST':
|
||||
raise errors.AccessForbiddenError('must be POST')
|
||||
|
||||
user = get_user_from_api_query_string() or get_request().user
|
||||
if self.trigger.roles:
|
||||
for role in self.trigger.roles:
|
||||
if role == logged_users_role().id and (user or is_url_signed()):
|
||||
break
|
||||
if role == '_submitter' and self.formdata.is_submitter(user):
|
||||
break
|
||||
if not user:
|
||||
continue
|
||||
if get_role_translation(self.formdata, role) in (user.roles or []):
|
||||
break
|
||||
else:
|
||||
raise errors.AccessForbiddenError('insufficient roles')
|
||||
|
||||
perform_items(self.action.items, self.formdata)
|
||||
return json.dumps({'err': 0})
|
||||
|
||||
|
||||
class WorkflowGlobalActionWebserviceHooksDirectory(Directory):
|
||||
def __init__(self, formdata):
|
||||
self.formdata = formdata
|
||||
|
||||
def _q_lookup(self, component):
|
||||
for action in self.formdata.formdef.workflow.global_actions:
|
||||
for trigger in action.triggers or []:
|
||||
if isinstance(trigger, WorkflowGlobalActionWebserviceTrigger):
|
||||
if trigger.identifier == component:
|
||||
return HookDirectory(self.formdata, action, trigger)
|
||||
raise errors.TraversalError()
|
|
@ -432,7 +432,12 @@ class Workflow(StorableObject):
|
|||
wf_status = formdata.get_status()
|
||||
if not wf_status: # draft
|
||||
return []
|
||||
return wf_status.get_subdirectories(formdata)
|
||||
directories = []
|
||||
for action in self.global_actions:
|
||||
for trigger in action.triggers or []:
|
||||
directories.extend(trigger.get_subdirectories(formdata))
|
||||
directories.extend(wf_status.get_subdirectories(formdata))
|
||||
return directories
|
||||
|
||||
def __setstate__(self, dict):
|
||||
self.__dict__.update(dict)
|
||||
|
@ -947,6 +952,9 @@ class WorkflowGlobalActionTrigger(XmlSerialisable):
|
|||
value = getattr(self, '%s_parse' % f)(value)
|
||||
setattr(self, f, value)
|
||||
|
||||
def get_subdirectories(self, formdata):
|
||||
return []
|
||||
|
||||
|
||||
class WorkflowGlobalActionManualTrigger(WorkflowGlobalActionTrigger):
|
||||
key = 'manual'
|
||||
|
@ -1156,6 +1164,39 @@ class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
|
|||
break
|
||||
|
||||
|
||||
class WorkflowGlobalActionWebserviceTrigger(WorkflowGlobalActionManualTrigger):
|
||||
key = 'webservice'
|
||||
identifier = None
|
||||
roles = None
|
||||
|
||||
def get_parameters(self):
|
||||
return ('identifier', 'roles')
|
||||
|
||||
def render_as_line(self):
|
||||
if self.identifier:
|
||||
return _('Webservice (%s)') % self.identifier
|
||||
else:
|
||||
return _('Webservice (not configured)')
|
||||
|
||||
def form(self, workflow):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add(StringWidget, 'identifier', title=_('Identifier'),
|
||||
required=True, value=self.identifier)
|
||||
options = [(None, '---', None)]
|
||||
options += workflow.get_list_of_roles(include_logged_in_users=True)
|
||||
form.add(WidgetList, 'roles', title=_('Roles'),
|
||||
element_type=SingleSelectWidget,
|
||||
value=self.roles,
|
||||
add_element_label=_('Add Role'),
|
||||
element_kwargs={'render_br': False,
|
||||
'options': options})
|
||||
return form
|
||||
|
||||
def get_subdirectories(self, formdata):
|
||||
from wcs.forms.workflows import WorkflowGlobalActionWebserviceHooksDirectory
|
||||
return [('hooks', WorkflowGlobalActionWebserviceHooksDirectory(formdata))]
|
||||
|
||||
|
||||
class WorkflowGlobalAction(object):
|
||||
id = None
|
||||
name = None
|
||||
|
@ -1183,7 +1224,8 @@ class WorkflowGlobalAction(object):
|
|||
def append_trigger(self, type):
|
||||
trigger_types = {
|
||||
'manual': WorkflowGlobalActionManualTrigger,
|
||||
'timeout': WorkflowGlobalActionTimeoutTrigger
|
||||
'timeout': WorkflowGlobalActionTimeoutTrigger,
|
||||
'webservice': WorkflowGlobalActionWebserviceTrigger,
|
||||
}
|
||||
o = trigger_types.get(type)()
|
||||
if not self.triggers:
|
||||
|
|
Loading…
Reference in New Issue