workflow: add workflow trace on form/carddata creation/edition (#77300) #310

Merged
lguerin merged 1 commits from wip/77300-workflow-trace-wf-action-on-form-card-data into main 2023-05-30 15:26:55 +02:00
9 changed files with 236 additions and 62 deletions

View File

@ -644,6 +644,14 @@ def test_inspect_page_actions_traces(pub):
]
target_formdef.store()
CardDef.wipe()
target_carddef = CardDef()
target_carddef.name = 'target card'
target_carddef.fields = [
fields.StringField(id='0', label='string', varname='foo_string'),
]
target_carddef.store()
workflow = Workflow.get_default_workflow()
workflow.id = '2'
workflow.criticality_levels = [
@ -656,12 +664,35 @@ def test_inspect_page_actions_traces(pub):
trigger = action.append_trigger('timeout')
trigger.anchor = 'creation'
trigger.timeout = '2'
create = workflow.possible_status[1].add_action('create_formdata', id='_create', prepend=True)
create.varname = 'create_formdata'
create.formdef_slug = target_formdef.url_name
create.mappings = [
Mapping(field_id='0', expression='foo bar'),
]
st1 = workflow.possible_status[1]
create_formdata = st1.add_action('create_formdata', id='_create', prepend=True)
create_formdata.varname = 'create_formdata'
create_formdata.formdef_slug = target_formdef.url_name
mappings = [Mapping(field_id='0', expression='foo bar')]
create_formdata.mappings = mappings
create_carddata = st1.add_action('create_carddata', id='_create_card')
create_carddata.label = 'create linked card'
create_carddata.formdef_slug = target_carddef.url_name
create_carddata.varname = 'created_card'
create_carddata.mappings = mappings
edit_carddata = st1.add_action('edit_carddata', id='_edit_card')
edit_carddata.label = 'edit linked card'
edit_carddata.formdef_slug = target_carddef.url_name
edit_carddata.varname = 'edited_card'
edit_carddata.mappings = [Mapping(field_id='0', expression='foo bar blah')]
edit_carddata2 = st1.add_action('edit_carddata', id='_edit_card2')
edit_carddata2.target_mode = 'manual'
edit_carddata2.target_id = '{{ unknown }}'
edit_carddata2.label = 'edit linked card with wrong target_id'
edit_carddata2.formdef_slug = target_carddef.url_name
edit_carddata2.varname = 'edited_card_wrong_target_id'
edit_carddata2.mappings = [Mapping(field_id='0', expression='foo bar blah')]
workflow.store()
formdef = FormDef()
@ -692,6 +723,9 @@ def test_inspect_page_actions_traces(pub):
assert [x.text for x in resp.pyquery('#inspect-timeline .event')] == [
'Created (frontoffice submission)',
'Continuation',
None, # Created form
None, # Created card
None, # Edited card
'Global action timeout',
]
assert [x.text for x in resp.pyquery('#inspect-timeline strong')] == ['Just Submitted', 'New']
@ -700,17 +734,33 @@ def test_inspect_page_actions_traces(pub):
'Email',
'Automatic Jump',
'New Form Creation',
'Create Card Data',
'Edit Card Data',
'Edit Card Data',
'Criticality Levels',
]
event_links = [x.attrib['href'] for x in resp.pyquery('#inspect-timeline .event a')]
assert event_links == []
assert event_links == [
'http://example.net/backoffice/management/target-form/1/', # Created form
'http://example.net/backoffice/data/target-card/1/', # Created card
'http://example.net/backoffice/data/target-card/1/', # Edited card
]
assert [x.text for x in resp.pyquery('#inspect-timeline .event a') if x.text] == [
'Created form - target form #1-1',
'Created card - target card #1-1',
'Edited card - target card #1-1',
]
assert [x.text for x in resp.pyquery('#inspect-timeline .event-error')] == ['Nothing edited']
action_links = [x.attrib['href'] for x in resp.pyquery('#inspect-timeline a.tracing-link')]
assert action_links[0] == 'http://example.net/backoffice/workflows/2/status/just_submitted/'
assert (
action_links[1]
== 'http://example.net/backoffice/workflows/2/status/just_submitted/items/_notify_new_receiver_email/'
)
assert action_links[-2] == 'http://example.net/backoffice/workflows/2/status/new/items/_create/'
assert action_links[-5] == 'http://example.net/backoffice/workflows/2/status/new/items/_create/'
assert action_links[-4] == 'http://example.net/backoffice/workflows/2/status/new/items/_create_card/'
assert action_links[-3] == 'http://example.net/backoffice/workflows/2/status/new/items/_edit_card/'
assert action_links[-2] == 'http://example.net/backoffice/workflows/2/status/new/items/_edit_card2/'
assert action_links[-1] == 'http://example.net/backoffice/workflows/2/global-actions/1/items/1/'
# check details are available

View File

@ -95,9 +95,14 @@ def test_create_carddata(pub):
# check tracing
carddata = carddef.data_class().select()[0]
trace = WorkflowTrace.select_for_formdata(carddata)[0]
assert trace.event == 'workflow-created'
assert trace.event_args['external_workflow_id'] == wf.id
assert trace.event_args['external_status_id'] == 'new'
assert trace.event_args['external_item_id'] == '_create'
trace = WorkflowTrace.select_for_formdata(formdata)[-1]
assert trace.event == 'workflow-created-carddata'
assert trace.event_args['external_formdef_id'] == carddef.id
assert trace.event_args['external_formdata_id'] == carddata.id
errors = pub.loggederror_class.select()
assert len(errors) == 2
@ -668,9 +673,14 @@ def test_edit_carddata_with_data_sourced_object(pub):
assert data.data['2_structured'] == {'id': 'c', 'text': 'cook', 'extra': 'plop2'}
# check evolutions & tracing
trace = WorkflowTrace.select_for_formdata(carddata)[0]
assert trace.event == 'workflow-edited'
assert trace.event_args['external_workflow_id'] == wf.id
assert trace.event_args['external_status_id'] == 'st1'
assert trace.event_args['external_item_id'] == 'edit'
trace = WorkflowTrace.select_for_formdata(formdata)[-1]
assert trace.event == 'workflow-edited-carddata'
assert trace.event_args['external_formdef_id'] == carddef.id
assert trace.event_args['external_formdata_id'] == carddata.id
formdata = formdef.data_class()()
formdata.data = {'0': '1', '1': 'b'}
@ -707,6 +717,28 @@ def test_edit_carddata_with_data_sourced_object(pub):
carddata.data = data.data
carddata.store()
# not target found
edit.mappings = [
Mapping(field_id='2', expression='{{ form_var_new_profession }}'),
]
edit.target_mode = 'manual'
edit.target_id = '{{ unknown }}'
wf.store()
formdata = formdef.data_class()()
formdata.data = {'0': '1', '1': 'c'}
formdata.store()
formdata.just_created()
formdata.store()
formdata.perform_workflow()
# no change
data = carddef.data_class().select()[0]
assert data.data['2'] == 'b'
assert data.data['2_display'] == 'baker'
assert data.data['2_structured'] == {'id': 'b', 'text': 'baker', 'extra': 'plop'}
trace = WorkflowTrace.select_for_formdata(formdata)[-1]
assert trace.event == 'workflow-edited-carddata'
assert trace.event_args == {}
def test_edit_carddata_with_linked_object(pub):
FormDef.wipe()

View File

@ -85,9 +85,14 @@ def test_create_formdata(pub):
# check evolutions & tracing
target_formdata = target_formdef.data_class().select()[0]
trace = WorkflowTrace.select_for_formdata(target_formdata)[0]
assert trace.event == 'workflow-created'
assert trace.event_args['external_workflow_id'] == wf.id
assert trace.event_args['external_status_id'] == 'new'
assert trace.event_args['external_item_id'] == '_create'
trace = WorkflowTrace.select_for_formdata(formdata)[-1]
assert trace.event == 'workflow-created-formdata'
assert trace.event_args['external_formdef_id'] == target_formdef.id
assert trace.event_args['external_formdata_id'] == target_formdata.id
errors = pub.loggederror_class.select()
assert len(errors) == 2

View File

@ -3803,68 +3803,21 @@ class FormBackOfficeStatusPage(FormStatusPage):
action_classes = {x.key: x.description for x in item_classes}
last_status_id = None
global_event = None
status_admin_base_url = '#'
for trace in self.workflow_traces:
if trace.event:
global_event = trace if trace.is_global_event() else None
if (
trace.event_args
and trace.event_args.get('external_workflow_id')
and trace.event_args.get('external_status_id')
and trace.event_args.get('external_item_id')
):
r += htmltext('<li><span class="event"><a href="%s">%s</a></span></li>') % (
trace.get_external_url(global_event),
trace.get_event_label(),
)
else:
r += htmltext('<li><span class="event">%s</span></li>') % trace.get_event_label()
r += htmltext(trace.print_event(global_event=global_event))
if trace.action_item_key:
action_label = action_classes.get(trace.action_item_key, trace.action_item_key)
try:
url = '%sitems/%s/' % (
trace.get_base_url(self.filled.formdef.workflow, trace.status_id, global_event),
trace.action_item_id,
)
except KeyError:
url = '#missing-%s' % trace.action_item_id
r += htmltext(
'<li><span class="datetime">%s</span> <a class="tracing-link" href="%s">%s</a>'
) % (
trace.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
url,
action_label,
trace.print_action(
action_classes=action_classes, filled=self.filled, global_event=global_event
)
)
real_action = trace.get_real_action(
self.filled.formdef.workflow,
trace.status_id,
trace.action_item_id,
global_event,
)
if real_action:
details = real_action.get_inspect_details()
if details:
r += htmltext(' <span class="tracing-details">(%s)</span>') % details
r += htmltext('</li>')
if trace.status_id != last_status_id:
last_status_id = trace.status_id
try:
status = self.filled.formdef.workflow.get_status(trace.status_id)
status_label = status.name
status_admin_base_url = status.get_admin_url()
except KeyError:
status_label = _('Unavailable status (%s)') % trace.status_id
status_admin_base_url = '#missing'
r += htmltext(
'<li><span class="datetime">%s</span> '
'<a class="tracing-link" href="%s"><strong>%s</strong></a></li>'
) % (
trace.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
status_admin_base_url,
status_label,
)
r += htmltext(trace.print_status(filled=self.filled))
return r.getvalue()

View File

@ -1796,13 +1796,18 @@ ul.form-inspector li {
}
}
span.event-datetime {
padding: 0 1ex 0 0;
visibility: hidden;
}
span.datetime,
span.status,
span.event {
span.event,
span.event-error {
padding: 0 1ex;
}
span.event {
span.event, span.event-error {
font-style: italic;
a {
border-bottom: none;

View File

@ -32,6 +32,7 @@ class CreateCarddataWorkflowStatusItem(CreateFormdataWorkflowStatusItem):
formdef_class = CardDef
evolution_part_class = LinkedCardDataEvolutionPart
workflow_trace_event = 'workflow-created-carddata'
formdef_label = _('Card')
mappings_label = _('Mappings to new card fields')

View File

@ -342,6 +342,7 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
formdef_class = FormDef
evolution_part_class = LinkedFormdataEvolutionPart
workflow_trace_event = 'workflow-created-formdata'
formdef_slug = None
formdef_label = _('Form')
@ -696,6 +697,12 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
external_item_id=self.id,
)
new_formdata.perform_workflow()
# add a link to created formdata
formdata.record_workflow_event(
self.workflow_trace_event,
external_formdef_id=formdef.id,
external_formdata_id=new_formdata.id,
)
# update local object as it may have been modified by new_formdata
# workflow execution.

View File

@ -52,6 +52,7 @@ class EditCarddataWorkflowStatusItem(CreateCarddataWorkflowStatusItem, ExternalW
formdata.store()
target_data = None
for target_data in self.iter_target_datas(formdata, carddef):
old_data = copy.deepcopy(target_data.data)
self.apply_mappings(dest=target_data, src=formdata)
@ -75,6 +76,17 @@ class EditCarddataWorkflowStatusItem(CreateCarddataWorkflowStatusItem, ExternalW
external_item_id=self.id,
)
target_data.store()
# add a link to created carddata
formdata.record_workflow_event(
'workflow-edited-carddata',
external_formdef_id=carddef.id,
external_formdata_id=target_data.id,
)
if target_data is None:
# nothing updated, no target_data found
formdata.record_workflow_event(
'workflow-edited-carddata',
)
# update local object as it may have modified itself
formdata.refresh_from_storage()

View File

@ -65,6 +65,9 @@ class WorkflowTrace(sql.WorkflowTrace):
'timeout-jump': _('Timeout jump'),
'workflow-created': _('Created (by workflow action)'),
'workflow-edited': _('Edited (by workflow action)'),
'workflow-created-formdata': _('Created form'),
'workflow-created-carddata': _('Created card'),
'workflow-edited-carddata': _('Edited card'),
'workflow-form-submit': _('Action in workflow form'),
}.get(self.event, self.event)
@ -81,6 +84,27 @@ class WorkflowTrace(sql.WorkflowTrace):
self._external_workflow = Workflow.get(self.event_args.get('external_workflow_id'))
return self._external_workflow
@property
def formdef(self):
from wcs.carddef import CardDef
from wcs.formdef import FormDef
if not hasattr(self, '_formdef'):
formdef_class = FormDef
if 'carddata' in self.event:
formdef_class = CardDef
self._formdef = formdef_class.get(self.event_args.get('external_formdef_id'), ignore_errors=True)
return self._formdef
@property
def formdata(self):
if not hasattr(self, '_formdata'):
self._formdata = self.formdef.data_class().get(
self.event_args.get('external_formdata_id'), ignore_errors=True
)
return self._formdata
def get_external_url(self, global_event):
try:
return '%sitems/%s/' % (
@ -94,6 +118,15 @@ class WorkflowTrace(sql.WorkflowTrace):
except KeyError:
return '#missing-%s' % self.event_args.get('external_item_id')
def get_external_formdata_url(self, global_event):
if self.formdef is None:
return '#missing-formdef-%s' % self.event_args.get('external_formdef_id')
if self.formdata is None:
return '#missing-formdata-%s' % self.event_args.get('external_formdata_id')
return self.formdata.get_backoffice_url()
def get_base_url(self, workflow, status_id, global_event):
if global_event:
if not global_event.event_args:
@ -126,3 +159,79 @@ class WorkflowTrace(sql.WorkflowTrace):
except IndexError:
real_action = None
return real_action
def print_event(self, global_event):
if (
self.event_args
and self.event_args.get('external_workflow_id')
and self.event_args.get('external_status_id')
and self.event_args.get('external_item_id')
):
return '<li><span class="event"><a href="%s">%s</a></span></li>' % (
self.get_external_url(global_event),
self.get_event_label(),
)
if (
self.event_args
and self.event_args.get('external_formdef_id')
and self.event_args.get('external_formdata_id')
):
return (
'<li><span class="event-datetime">%s</span><span class="event"><a href="%s">%s - %s</a></span></li>'
% (
self.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
self.get_external_formdata_url(global_event),
self.get_event_label(),
self.formdata.get_display_name() if self.formdata else _('deleted'),
)
)
if self.event == 'workflow-edited-carddata':
return '<li><span class="event-datetime">%s</span><span class="event-error">%s</span></li>' % (
self.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
_('Nothing edited'),
)
return '<li><span class="event">%s</span></li>' % self.get_event_label()
def print_action(self, action_classes, filled, global_event):
action_label = action_classes.get(self.action_item_key, self.action_item_key)
try:
url = '%sitems/%s/' % (
self.get_base_url(filled.formdef.workflow, self.status_id, global_event),
self.action_item_id,
)
except KeyError:
url = '#missing-%s' % self.action_item_id
r = '<li><span class="datetime">%s</span> <a class="tracing-link" href="%s">%s</a>' % (
self.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
url,
action_label,
)
real_action = self.get_real_action(
filled.formdef.workflow,
self.status_id,
self.action_item_id,
global_event,
)
if real_action:
details = real_action.get_inspect_details()
if details:
r += ' <span class="tracing-details">(%s)</span>' % details
r += '</li>'
return r
def print_status(self, filled):
try:
status = filled.formdef.workflow.get_status(self.status_id)
status_label = status.name
status_admin_base_url = status.get_admin_url()
except KeyError:
status_label = _('Unavailable status (%s)') % self.status_id
status_admin_base_url = '#missing'
return (
'<li><span class="datetime">%s</span> '
'<a class="tracing-link" href="%s"><strong>%s</strong></a></li>'
) % (
self.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
status_admin_base_url,
status_label,
)