From 19b99bea580a7019c8e1991f3811797bd6465ad8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Tue, 1 Jun 2021 22:25:07 +0200
Subject: [PATCH 2/2] backoffice: add tracing information to inspect page
 (#54497)

---
 tests/backoffice_pages/test_all.py   | 24 ++++++++++
 wcs/backoffice/management.py         | 69 +++++++++++++++++++++++++++-
 wcs/qommon/static/css/dc2/admin.scss | 18 ++++++++
 wcs/workflows.py                     | 29 +++++++++++-
 4 files changed, 138 insertions(+), 2 deletions(-)

diff --git a/tests/backoffice_pages/test_all.py b/tests/backoffice_pages/test_all.py
index f652e9e20..f3e64561d 100644
--- a/tests/backoffice_pages/test_all.py
+++ b/tests/backoffice_pages/test_all.py
@@ -5022,6 +5022,30 @@ def test_inspect_page_with_related_objects(pub):
     )
 
 
+def test_inspect_page_actions_traces(pub):
+    create_environment(pub)
+    create_user(pub, is_admin=True)
+
+    formdef = FormDef.get_by_urlname('form-title')
+
+    formdef.data_class().wipe()
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.just_created()
+    formdata.perform_workflow()
+    formdata.store()
+
+    resp = login(get_app(pub)).get(formdata.get_url(backoffice=True), status=200)
+    resp = resp.click('Data Inspector')
+    assert '<h2>Actions Tracing</h2>' in resp
+    assert [x.text for x in resp.pyquery('#inspect-timeline strong')] == ['Just Submitted', 'New']
+    assert [x.text for x in resp.pyquery('#inspect-timeline a.tracing-link') if x.text] == [
+        'Email',
+        'Email',
+        'Automatic Jump',
+    ]
+
+
 def test_workflow_jump_previous(pub):
     user = create_user(pub)
     create_environment(pub)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 45289514a..6705de6fa 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -42,7 +42,7 @@ from wcs.forms.backoffice import FormDefUI
 from wcs.forms.common import FormStatusPage
 from wcs.roles import logged_users_role
 from wcs.variables import LazyFieldVar
-from wcs.workflows import WorkflowStatusItem, template_on_formdata
+from wcs.workflows import ActionsTracingEvolutionPart, WorkflowStatusItem, item_classes, template_on_formdata
 
 from ..qommon import _, emails, errors, ezt, force_str, get_cfg, get_logger, misc, ngettext, ods, sms
 from ..qommon.admin.emails import EmailsDirectory
@@ -3427,6 +3427,73 @@ class FormBackOfficeStatusPage(FormStatusPage):
             r += htmltext('</ul>')
             r += htmltext('</div>')
 
+        has_tracing = False
+        for evolution in self.filled.evolution or []:
+            if evolution.parts and any(isinstance(x, ActionsTracingEvolutionPart) for x in evolution.parts):
+                has_tracing = True
+                break
+
+        if has_tracing:
+            action_classes = {x.key: x.description for x in item_classes}
+            r += htmltext('<div id="inspect-timeline" class="section">')
+            r += htmltext('<h2>%s</h2></li>\n') % _('Actions Tracing')
+            r += htmltext('<ul class="form-inspector biglist">')
+            wf_status = None
+            status_admin_base_url = '#'
+            for evolution in self.filled.evolution:
+                if evolution.status and evolution.status != wf_status:
+                    for part in evolution.parts or []:
+                        if isinstance(part, ActionsTracingEvolutionPart):
+                            if part.actions:
+                                r += (
+                                    htmltext('<li><span class="event">%s</span></li>')
+                                    % part.get_event_label()
+                                )
+                                break
+                    try:
+                        status = self.filled.formdef.workflow.get_status(evolution.status)
+                        status_label = status.name
+                        status_admin_base_url = status.get_admin_url()
+                    except KeyError:
+                        status_label = _('Unavailable status (%s)') % evolution.status
+                        status_admin_base_url = '#missing'
+                    r += htmltext(
+                        '<li><span class="datetime">%s</span> '
+                        '<a class="tracing-link" href="%s"><strong>%s</strong></a></li>'
+                    ) % (
+                        time.strftime('%Y-%m-%d %H:%M:%S', evolution.time) if evolution.time else '-',
+                        status_admin_base_url,
+                        status_label,
+                    )
+                if evolution.status:
+                    wf_status = evolution.status
+                first_part = True
+                for part in evolution.parts or []:
+                    if isinstance(part, ActionsTracingEvolutionPart):
+                        if not first_part and part.actions:
+                            r += htmltext('<li><span class="event">%s</span></li>') % part.get_event_label()
+                        first_part = False
+                        for action_ts, action_key, action_id in part.actions:
+                            action_label = action_classes.get(action_key, action_key)
+                            try:
+                                url = '%sitems/%s/' % (
+                                    part.get_base_url(self.filled.formdef.workflow, wf_status),
+                                    action_id,
+                                )
+                            except KeyError:
+                                url = '#missing-%s' % action_id
+                            r += htmltext(
+                                '<li><span class="datetime">%s</span> '
+                                '<a class="tracing-link" href="%s">%s</a></li>'
+                            ) % (
+                                action_ts.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
+                                url,
+                                action_label,
+                            )
+
+            r += htmltext('</ul>')
+            r += htmltext('</div>')
+
         # markers stack
         if '_markers_stack' in (self.filled.workflow_data or {}):
             r += htmltext('<div id="inspect-markers" class="section">')
diff --git a/wcs/qommon/static/css/dc2/admin.scss b/wcs/qommon/static/css/dc2/admin.scss
index 986c71665..8d97b33a3 100644
--- a/wcs/qommon/static/css/dc2/admin.scss
+++ b/wcs/qommon/static/css/dc2/admin.scss
@@ -1662,10 +1662,28 @@ ul.form-inspector li code {
 	font-size: 100%;
 }
 
+ul.form-inspector li span.datetime {
+	padding: 0 1ex;
+}
+
+ul.form-inspector li a.tracing-link {
+	display: inline;
+	padding: 0;
+	&:hover {
+		background: transparent;
+		text-decoration: underline;
+	}
+}
+
 ul.form-inspector li span.status {
 	padding: 0 1ex;
 }
 
+ul.form-inspector span.event {
+	padding: 0 1ex;
+	font-style: italic;
+}
+
 ul.form-inspector li div.value {
 	display: block;
 	padding: 0 0 0 1em;
diff --git a/wcs/workflows.py b/wcs/workflows.py
index ce90c6313..c2ef88a46 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -80,7 +80,7 @@ def perform_items(items, formdata, depth=20, event=None):
             continue
         if not item.check_condition(formdata):
             continue
-        performed_actions.append((datetime.datetime.now(), item.id))
+        performed_actions.append((datetime.datetime.now(), item.key, item.id))
         try:
             url = item.perform(formdata) or url
         except AbortActionException as e:
@@ -340,6 +340,33 @@ class ActionsTracingEvolutionPart(EvolutionPart):
             self.event_args = None
         self.actions = actions
 
+    def get_event_label(self):
+        return {
+            'api-created': _('Created (by API)'),
+            'api-post-edit-action': _('Actions after edit action (by APIà'),
+            'api-trigger': _('API Trigger'),
+            'backoffice-created': _('Created (backoffice submission)'),
+            'continuation': _('Continuation'),
+            'csv-import-created': _('Created (by CSV import)'),
+            'edit-action': _('Actions after edit action'),
+            'frontoffice-created': _('Created (frontoffice submission)'),
+            'global-action-button': _('Click on a global action button'),
+            'global-action': _('Global action'),
+            'global-action-timeout': _('Global action timeout'),
+            'timeout-jump': _('Timeout jump'),
+            'workflow-created': _('Created (by workflow action)'),
+            'workflow-form-submit': _('Action in workflow form'),
+        }.get(self.event, self.event)
+
+    def is_global_event(self):
+        return bool(self.event and self.event.startswith('global-'))
+
+    def get_base_url(self, workflow, status_id):
+        if self.is_global_event():
+            return '%sglobal-actions/%s/' % (workflow.get_admin_url(), self.event_args[0])
+        status = workflow.get_status(status_id)
+        return status.get_admin_url()
+
 
 class DuplicateGlobalActionNameError(Exception):
     pass
-- 
2.32.0

