From 5cce913aab38e942f7ff15cf0d853f01099fc603 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 11 Oct 2022 11:26:43 +0200
Subject: [PATCH 1/2] workflow: add a trace on update carddata by wf action
 (#70000)

---
 tests/workflow/test_carddata.py      |  5 +++++
 wcs/backoffice/management.py         | 20 +++++++++++++++++---
 wcs/qommon/static/css/dc2/admin.scss | 10 +++++++++-
 wcs/wf/edit_carddata.py              | 19 ++++++++++++++++++-
 wcs/workflows.py                     | 28 +++++++++++++++++++++++++++-
 5 files changed, 76 insertions(+), 6 deletions(-)

diff --git a/tests/workflow/test_carddata.py b/tests/workflow/test_carddata.py
index c08b825fa..1755dfb54 100644
--- a/tests/workflow/test_carddata.py
+++ b/tests/workflow/test_carddata.py
@@ -634,6 +634,11 @@ def test_edit_carddata_with_data_sourced_object(pub):
     assert data.data['2'] == 'c'
     assert data.data['2_display'] == 'cook'
     assert data.data['2_structured'] == {'id': 'c', 'text': 'cook', 'extra': 'plop2'}
+    # check evolutions & tracing
+    assert isinstance(data.evolution[0].parts[0], ActionsTracingEvolutionPart)
+    assert data.evolution[0].parts[0].external_workflow_id == wf.id
+    assert data.evolution[0].parts[0].external_status_id == 'st1'
+    assert data.evolution[0].parts[0].external_item_id == 'edit'
 
     formdata = formdef.data_class()()
     formdata.data = {'0': '1', '1': 'b'}
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 9ff70a9f5..49967d592 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -3721,6 +3721,13 @@ class FormBackOfficeStatusPage(FormStatusPage):
             if evolution.status and evolution.status != wf_status:
                 for part in evolution.parts or []:
                     if isinstance(part, ActionsTracingEvolutionPart):
+                        if part.external_workflow and part.external_status_id and part.external_item_id:
+                            r += htmltext('<li><span class="event"><a href="%s">%s</a></span></li>') % (
+                                part.get_external_url(),
+                                part.get_event_label(),
+                            )
+                            last_event_line = part
+                            break
                         if part.actions:
                             r += htmltext('<li><span class="event">%s</span></li>') % part.get_event_label()
                             last_event_line = part
@@ -3744,9 +3751,16 @@ class FormBackOfficeStatusPage(FormStatusPage):
                 wf_status = evolution.status
             for part in evolution.parts or []:
                 if isinstance(part, ActionsTracingEvolutionPart):
-                    if part.actions and part != last_event_line:
-                        last_event_line = part
-                        r += htmltext('<li><span class="event">%s</span></li>') % part.get_event_label()
+                    if part != last_event_line:
+                        if part.external_workflow and part.external_status_id and part.external_item_id:
+                            last_event_line = part
+                            r += htmltext('<li><span class="event"><a href="%s">%s</a></span></li>') % (
+                                part.get_external_url(),
+                                part.get_event_label(),
+                            )
+                        elif part.actions:
+                            last_event_line = part
+                            r += htmltext('<li><span class="event">%s</span></li>') % part.get_event_label()
                     for action_ts, action_key, action_id in part.actions:
                         action_label = action_classes.get(action_key, action_key)
                         try:
diff --git a/wcs/qommon/static/css/dc2/admin.scss b/wcs/qommon/static/css/dc2/admin.scss
index c6f57ea89..48327a03a 100644
--- a/wcs/qommon/static/css/dc2/admin.scss
+++ b/wcs/qommon/static/css/dc2/admin.scss
@@ -1797,6 +1797,14 @@ ul.form-inspector li {
 
 	span.event {
 		font-style: italic;
+		a {
+			border-bottom: none;
+			color: #215D9C;
+			&:hover {
+				background: transparent;
+				text-decoration: underline;
+			}
+		}
 	}
 
 	a.tracing-link {
@@ -2502,4 +2510,4 @@ div#main-content > h3.field-edit--subtitle {
 #workflow-fullscreen-schema {
 	height: 95vh;
 	width: 100%;
-}
\ No newline at end of file
+}
diff --git a/wcs/wf/edit_carddata.py b/wcs/wf/edit_carddata.py
index 9152d3033..50ce42287 100644
--- a/wcs/wf/edit_carddata.py
+++ b/wcs/wf/edit_carddata.py
@@ -14,12 +14,15 @@
 # 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 time
+
 from quixote import get_publisher
 
+from wcs.formdata import Evolution
 from wcs.qommon import _
 from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
 from wcs.wf.external_workflow import ExternalWorkflowGlobalAction
-from wcs.workflows import register_item_class
+from wcs.workflows import ActionsTracingEvolutionPart, register_item_class
 
 
 class EditCarddataWorkflowStatusItem(CreateCarddataWorkflowStatusItem, ExternalWorkflowGlobalAction):
@@ -51,6 +54,20 @@ class EditCarddataWorkflowStatusItem(CreateCarddataWorkflowStatusItem, ExternalW
         for target_data in self.iter_target_datas(formdata, carddef):
             self.apply_mappings(dest=target_data, src=formdata)
             with get_publisher().substitutions.freeze():
+                # store an ActionsTracingEvolutionPart with a link to current workflow & action
+                evo = Evolution()
+                evo.time = time.localtime()
+                evo.status = target_data.status
+                evo.add_part(
+                    ActionsTracingEvolutionPart(
+                        'workflow-edited',
+                        [],
+                        external_workflow_id=self.parent.parent.id,
+                        external_status_id=self.parent.id,
+                        external_item_id=self.id,
+                    )
+                )
+                target_data.evolution.append(evo)
                 target_data.store()
 
         # update local object as it may have modified itself
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 504ffd9fc..6f1607242 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -390,7 +390,9 @@ class AttachmentEvolutionPart(EvolutionPart):
 
 
 class ActionsTracingEvolutionPart(EvolutionPart):
-    def __init__(self, event, actions):
+    def __init__(
+        self, event, actions, external_workflow_id=None, external_status_id=None, external_item_id=None
+    ):
         if isinstance(event, tuple):
             self.event = event[0]
             self.event_args = event[1:]
@@ -398,6 +400,9 @@ class ActionsTracingEvolutionPart(EvolutionPart):
             self.event = event
             self.event_args = None
         self.actions = actions
+        self.external_workflow_id = external_workflow_id
+        self.external_status_id = external_status_id
+        self.external_item_id = external_item_id
 
     def get_event_label(self):
         return {
@@ -419,12 +424,33 @@ class ActionsTracingEvolutionPart(EvolutionPart):
             'json-import-created': _('Created (by JSON import)'),
             'timeout-jump': _('Timeout jump'),
             'workflow-created': _('Created (by workflow action)'),
+            'workflow-edited': _('Edited (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-'))
 
+    @property
+    def external_workflow(self):
+        if not hasattr(self, '_external_workflow'):
+            self._external_workflow = None
+            if self.external_workflow_id:
+                self._external_workflow = Workflow.get(self.external_workflow_id, ignore_errors=True)
+        return self._external_workflow
+
+    def get_external_url(self):
+        try:
+            return '%sitems/%s/' % (
+                self.get_base_url(
+                    self.external_workflow,
+                    self.external_status_id,
+                ),
+                self.external_item_id,
+            )
+        except KeyError:
+            return '#missing-%s' % self.external_item_id
+
     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])
-- 
2.35.1

