workflows: add actions tracing (#54497)

This commit is contained in:
Frédéric Péters 2021-06-01 21:17:12 +02:00
parent 739a53eb5a
commit 622a2cb5b3
11 changed files with 75 additions and 29 deletions

View File

@ -9,7 +9,7 @@ from wcs.formdef import FormDef
from wcs.qommon.http_request import HTTPRequest
from wcs.qommon.ident.password_accounts import PasswordAccount
from wcs.wf.jump import JumpWorkflowStatusItem
from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
from wcs.wf.register_comment import JournalEvolutionPart, RegisterCommenterWorkflowStatusItem
from wcs.workflows import Workflow
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app
@ -370,6 +370,13 @@ def test_workflow_trigger_http_auth_access(pub, local_user):
assert formdef.data_class().get(formdata.id).evolution[-1].who is None
def get_latest_comment(formdata):
for evolution in reversed(formdata.evolution):
for part in reversed(evolution.parts):
if isinstance(part, JournalEvolutionPart):
return part.content
def test_workflow_global_webservice_trigger(pub, local_user):
workflow = Workflow(name='test')
workflow.add_status('Status1', 'st1')
@ -405,12 +412,12 @@ def test_workflow_global_webservice_trigger(pub, local_user):
# anonymous call
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'
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD'
add_to_journal.comment = 'HELLO WORLD 2'
workflow.store()
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'
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD 2'
# call requiring user
add_to_journal.comment = 'HELLO WORLD 3'
@ -418,7 +425,7 @@ def test_workflow_global_webservice_trigger(pub, local_user):
workflow.store()
get_app(pub).post(formdata.get_api_url() + 'hooks/plop/', status=403)
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'
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD 3'
# call requiring roles
add_to_journal.comment = 'HELLO WORLD 4'
@ -435,7 +442,7 @@ def test_workflow_global_webservice_trigger(pub, local_user):
local_user.roles = [role.id]
local_user.store()
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'
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD 4'
# call adding data
add_to_journal.comment = 'HELLO {{plop_test}}'
@ -444,7 +451,7 @@ def test_workflow_global_webservice_trigger(pub, local_user):
sign_uri(formdata.get_api_url() + 'hooks/plop/', user=local_user), {'test': 'foobar'}, status=200
)
# (django templating make it turn into HTML)
assert formdef.data_class().get(formdata.id).evolution[-1].parts[-1].content == '<div>HELLO foobar</div>'
assert get_latest_comment(formdef.data_class().get(formdata.id)) == '<div>HELLO foobar</div>'
# call adding data but with no actions
ac1.items = []
@ -490,4 +497,4 @@ def test_workflow_global_webservice_trigger_no_trailing_slash(pub, local_user):
# anonymous call
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'
assert get_latest_comment(formdef.data_class().get(formdata.id)) == 'HELLO WORLD'

View File

@ -1705,7 +1705,6 @@ def test_formdata_evolution_registercommenter_to_with_attachment(pub):
assert 'The form has been recorded' in resp.text
formdata = formdef.data_class().select()[0]
assert len(formdata.evolution[0].parts) == 6
resp = app.get('/test/%s/' % formdata.id)
resp.status_int = 200

View File

@ -164,7 +164,7 @@ class ApiFormdataPage(FormStatusPage):
if item.status:
self.formdata.jump_status(item.status)
self.formdata.perform_workflow()
self.formdata.perform_workflow(event=('api-post-edit-action', item.id))
return json.dumps({'err': 0, 'data': {'id': str(self.formdata.id)}})
@ -323,7 +323,7 @@ class ApiCardPage(ApiFormPageMixin, BackofficeCardPage):
formdata.store()
formdata.just_created()
formdata.store()
formdata.perform_workflow()
formdata.perform_workflow(event='api-created')
formdata.store()
return json.dumps(
{
@ -615,7 +615,7 @@ class ApiFormdefDirectory(Directory):
else:
formdata.just_created()
formdata.store()
formdata.perform_workflow()
formdata.perform_workflow(event='api-created')
formdata.store()
return json.dumps(
{

View File

@ -370,7 +370,7 @@ class ImportFromCsvAfterJob(AfterJob):
data_instance.data = item
data_instance.just_created()
data_instance.store()
data_instance.perform_workflow()
data_instance.perform_workflow(event='csv-import-created')
def done_action_url(self):
carddef = self.kwargs['carddef_class'].get(self.kwargs['carddef_id'])

View File

@ -350,7 +350,7 @@ class FormFillPage(PublicFormFillPage):
return self.redirect_after_submitted(form, filled)
def redirect_after_submitted(self, form, filled):
url = filled.perform_workflow()
url = filled.perform_workflow(event='backoffice-created')
if url:
pass # always redirect to an URL the workflow returned
elif not self.formdef.is_of_concern_for_user(self.user, filled):

View File

@ -536,13 +536,13 @@ class FormData(StorableObject):
current_level = current_level - 100
return levels[current_level]
def perform_workflow(self):
def perform_workflow(self, event=None):
url = None
get_publisher().substitutions.feed(self)
wf_status = self.get_status()
from wcs.workflows import perform_items
url = perform_items(wf_status.items, self)
url = perform_items(wf_status.items, self, event=event)
return url
def perform_global_action(self, action_id, user):
@ -551,7 +551,7 @@ class FormData(StorableObject):
for action in self.formdef.workflow.get_global_actions_for_user(formdata=self, user=user):
if action.id != action_id:
continue
perform_items(action.items, self)
perform_items(action.items, self, event=('global-action', action.id))
break
def get_workflow_messages(self, position='top', user=None):
@ -656,6 +656,8 @@ class FormData(StorableObject):
return None
def jump_status(self, status_id, user_id=None):
from wcs.workflows import ActionsTracingEvolutionPart
if status_id == '_previous':
previous_status = self.pop_previous_marked_status()
if not previous_status:
@ -670,7 +672,9 @@ class FormData(StorableObject):
self.status == status
and self.evolution[-1].status == status
and not self.evolution[-1].comment
and not self.evolution[-1].parts
and not [
x for x in self.evolution[-1].parts or [] if not isinstance(x, ActionsTracingEvolutionPart)
]
):
# if status do not change and last evolution is empty,
# just update last jump time on last evolution, do not add one

View File

@ -1467,7 +1467,7 @@ class FormPage(Directory, FormTemplateMixin):
url = None
if existing_formdata is None:
self.clean_submission_context()
url = filled.perform_workflow()
url = filled.perform_workflow(event='frontoffice-created')
if not filled.user_id:
get_session().mark_anonymous_formdata(filled)
@ -1523,7 +1523,7 @@ class FormPage(Directory, FormTemplateMixin):
wf_status = item.get_target_status(self.edited_data)
if wf_status:
self.edited_data.jump_status(wf_status[0].id, user_id=user_id)
url = self.edited_data.perform_workflow()
url = self.edited_data.perform_workflow(event=('edit-action', item.id))
else:
# add history entry
evo = Evolution()

View File

@ -895,6 +895,7 @@ class StorableObject:
def remove_self(self):
assert not self.is_readonly()
self.remove_object(self.id)
self.id = None
def get_last_modification_info(self):
if not get_publisher().snapshot_class:

View File

@ -507,7 +507,7 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
new_formdata.store()
if formdef.enable_tracking_codes:
code.formdata = new_formdata # this will .store() the code
new_formdata.perform_workflow()
new_formdata.perform_workflow(event=('workflow-created', formdata.get_display_id()))
new_formdata.store()
# update local object as it may have been modified by new_formdata

View File

@ -40,13 +40,13 @@ from ..qommon.template import Template
JUMP_TIMEOUT_INTERVAL = max((60 // int(os.environ.get('WCS_JUMP_TIMEOUT_CHECKS', '3')), 1))
def jump_and_perform(formdata, action, workflow_data=None):
def jump_and_perform(formdata, action, workflow_data=None, event=None):
action.handle_markers_stack(formdata)
if workflow_data:
formdata.update_workflow_data(workflow_data)
formdata.store()
formdata.jump_status(action.status)
url = formdata.perform_workflow()
url = formdata.perform_workflow(event=event)
return url
@ -89,7 +89,9 @@ class TriggerDirectory(Directory):
workflow_data = None
if hasattr(get_request(), '_json'):
workflow_data = get_request().json
url = jump_and_perform(self.formdata, item, workflow_data=workflow_data)
url = jump_and_perform(
self.formdata, item, workflow_data=workflow_data, event=('api-trigger', item.trigger)
)
else:
if get_request().is_json():
get_response().status_code = 403
@ -354,7 +356,7 @@ def _apply_timeouts(publisher):
get_publisher().substitutions.feed(formdef)
get_publisher().substitutions.feed(formdata)
if jump_action.must_jump(formdata):
jump_and_perform(formdata, jump_action)
jump_and_perform(formdata, jump_action, event=('timeout-jump', jump_action.id))
break

View File

@ -68,14 +68,18 @@ def lax_int(s):
return -1
def perform_items(items, formdata, depth=20):
def perform_items(items, formdata, depth=20, event=None):
if depth == 0: # prevents infinite loops
return
url = None
old_status = formdata.status
performed_actions = []
for item in items:
if getattr(item.perform, 'noop', False):
continue
if not item.check_condition(formdata):
continue
performed_actions.append((datetime.datetime.now(), item.id))
try:
url = item.perform(formdata) or url
except AbortActionException as e:
@ -83,6 +87,13 @@ def perform_items(items, formdata, depth=20):
break
if formdata.status != old_status:
break
if performed_actions:
latest_evolution = formdata.evolution[-1] if formdata.evolution else None
if latest_evolution:
latest_evolution.add_part(ActionsTracingEvolutionPart(event, performed_actions))
if formdata.id:
# don't save formdata it has been removed
formdata.store()
if formdata.status != old_status:
if not formdata.evolution:
formdata.evolution = []
@ -93,7 +104,7 @@ def perform_items(items, formdata, depth=20):
formdata.store()
# performs the items of the new status
wf_status = formdata.get_status()
url = perform_items(wf_status.items, formdata, depth=depth - 1) or url
url = perform_items(wf_status.items, formdata, depth=depth - 1, event='continuation') or url
if url:
# hack around webtest as it checks type(url) is str and
# this won't work on django safe strings (isinstance would work);
@ -322,6 +333,17 @@ class AttachmentEvolutionPart(EvolutionPart):
)
class ActionsTracingEvolutionPart(EvolutionPart):
def __init__(self, event, actions):
if isinstance(event, tuple):
self.event = event[0]
self.event_args = event[1:]
else:
self.event = event
self.event_args = None
self.actions = actions
class DuplicateGlobalActionNameError(Exception):
pass
@ -1459,7 +1481,11 @@ class WorkflowGlobalActionTimeoutTrigger(WorkflowGlobalActionTrigger):
continue
formdata.evolution[-1].add_part(WorkflowGlobalActionTimeoutTriggerMarker(trigger.id))
formdata.store()
perform_items(action.items, formdata)
perform_items(
action.items,
formdata,
event=('global-action-timeout', (action.id, trigger.id)),
)
break
@ -1706,7 +1732,7 @@ class WorkflowStatus:
# check for global actions
for action in filled.formdef.workflow.get_global_actions_for_user(filled, user):
if 'button-action-%s' % action.id in get_request().form:
url = perform_items(action.items, filled)
url = perform_items(action.items, filled, event=('global-action-button', action.id))
if url:
return url
return
@ -1746,7 +1772,7 @@ class WorkflowStatus:
if evo.status:
filled.status = evo.status
filled.store()
url = filled.perform_workflow()
url = filled.perform_workflow(event='workflow-form-submit')
if url:
return url
@ -1908,6 +1934,12 @@ class WorkflowStatus:
return '<%s %s %r>' % (self.__class__.__name__, self.id, self.name)
def noop_mark(func):
# mark method as not executing anything
func.noop = True
return func
class WorkflowStatusItem(XmlSerialisable):
node_name = 'item'
description = 'XX'
@ -1960,6 +1992,7 @@ class WorkflowStatusItem(XmlSerialisable):
def get_add_role_label(self):
return self.parent.parent.get_add_role_label()
@noop_mark
def perform(self, formdata):
pass