From dc2cd97f7089508368e0ebb381b0ba5bdd9c8615 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 7 Oct 2022 16:20:33 +0200
Subject: [PATCH] workflow: register a part on user assignation error (#69395)

---
 tests/form_pages/test_all.py    |  9 ++++--
 tests/test_formdata.py          | 11 ++++++--
 tests/workflow/test_carddata.py | 21 ++++++++------
 wcs/wf/create_formdata.py       | 50 +++++++++++++++++++++++++++++----
 4 files changed, 72 insertions(+), 19 deletions(-)

diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index b94a479f4..a9f53c14b 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -30,7 +30,7 @@ from wcs.qommon.substitution import CompatibilityNamesDict
 from wcs.qommon.template import Template
 from wcs.roles import logged_users_role
 from wcs.tracking_code import TrackingCode
-from wcs.wf.create_formdata import Mapping
+from wcs.wf.create_formdata import JournalAssignationErrorPart, Mapping
 from wcs.wf.form import WorkflowFormFieldsFormDef
 from wcs.wf.wscall import JournalWsCallErrorPart
 from wcs.workflows import (
@@ -5684,7 +5684,7 @@ def test_form_page_profile_verified_prefill(pub):
         assert formdatas[0].data['0'] == 'foo@localhost'
 
 
-def test_form_worklow_multiple_identical_status_with_wserror(pub):
+def test_form_worklow_multiple_identical_status_with_journal_error(pub):
     user = create_user(pub)
 
     formdef = create_formdef()
@@ -5724,6 +5724,7 @@ def test_form_worklow_multiple_identical_status_with_wserror(pub):
         resp = app.post(formdata.get_url() + 'jump/trigger/XXX', status=302)
         formdata = formdef.data_class().get(formdata.id)
         formdata.evolution[-1].add_part(JournalWsCallErrorPart('bla', 'bla', {}))
+        formdata.evolution[-1].add_part(JournalAssignationErrorPart('foo', 'foo'))
         formdata.store()
 
     # mark user as owner so it can check the UI
@@ -5735,7 +5736,9 @@ def test_form_worklow_multiple_identical_status_with_wserror(pub):
     role.allows_backoffice_access = True
     role.store()
     resp = app.get(formdata.get_url(backoffice=True))
-    assert resp.text.count('<li class="msg') == 3
+    assert len(resp.pyquery('div.msg')) == 3
+    assert len(resp.pyquery('div.msg div.ws-error')) == 3
+    assert len(resp.pyquery('div.msg div.assignation-error')) == 3
 
 
 def test_form_page_verified_prefill_error_page(pub):
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index d9a3dfe57..9d00bca48 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -26,6 +26,7 @@ from wcs.qommon.substitution import CompatibilityNamesDict
 from wcs.qommon.template import Template
 from wcs.qommon.upload_storage import PicklableUpload
 from wcs.variables import LazyFormData
+from wcs.wf.create_formdata import JournalAssignationErrorPart
 from wcs.wf.register_comment import JournalEvolutionPart
 from wcs.wf.wscall import JournalWsCallErrorPart
 from wcs.workflows import (
@@ -493,6 +494,7 @@ def test_get_json_export_dict_evolution(pub, local_user):
     d.store()
     evo.add_part(JournalEvolutionPart(d, "ok", None))
     evo.add_part(JournalWsCallErrorPart("summary", "label", "data"))
+    evo.add_part(JournalAssignationErrorPart("summary", "label"))
     evo = Evolution()
     evo.time = time.localtime()
     evo.status = 'wf-%s' % st_finished.id
@@ -509,13 +511,16 @@ def test_get_json_export_dict_evolution(pub, local_user):
     assert export['evolution'][0]['who']['email'] == local_user.email
     assert export['evolution'][0]['who']['NameID'] == local_user.name_identifiers
     assert 'parts' in export['evolution'][0]
-    assert len(export['evolution'][0]['parts']) == 2
+    assert len(export['evolution'][0]['parts']) == 3
     assert export['evolution'][0]['parts'][0]['type'] == 'workflow-comment'
     assert export['evolution'][0]['parts'][0]['content'] == 'ok'
     assert export['evolution'][0]['parts'][1]['type'] == 'wscall-error'
     assert export['evolution'][0]['parts'][1]['summary'] == 'summary'
     assert export['evolution'][0]['parts'][1]['label'] == 'label'
     assert export['evolution'][0]['parts'][1]['data'] == 'data'
+    assert export['evolution'][0]['parts'][2]['type'] == 'assignation-error'
+    assert export['evolution'][0]['parts'][2]['summary'] == 'summary'
+    assert export['evolution'][0]['parts'][2]['label'] == 'label'
     assert export['evolution'][1]['status'] == st_finished.id
     assert 'time' in export['evolution'][1]
     assert export['evolution'][1]['who']['id'] == local_user.id
@@ -530,11 +535,13 @@ def test_get_json_export_dict_evolution(pub, local_user):
     assert 'time' in export['evolution'][0]
     assert 'who' not in export['evolution'][0]
     assert 'parts' in export['evolution'][0]
-    assert len(export['evolution'][0]['parts']) == 2
+    assert len(export['evolution'][0]['parts']) == 3
     assert len(export['evolution'][0]['parts'][0]) == 2
     assert export['evolution'][0]['parts'][0]['type'] == 'workflow-comment'
     assert len(export['evolution'][0]['parts'][1]) == 1
     assert export['evolution'][0]['parts'][1]['type'] == 'wscall-error'
+    assert len(export['evolution'][0]['parts'][2]) == 1
+    assert export['evolution'][0]['parts'][2]['type'] == 'assignation-error'
     assert export['evolution'][1]['status'] == st_finished.id
     assert 'time' in export['evolution'][1]
     assert 'who' not in export['evolution'][0]
diff --git a/tests/workflow/test_carddata.py b/tests/workflow/test_carddata.py
index c08b825fa..dc095b60d 100644
--- a/tests/workflow/test_carddata.py
+++ b/tests/workflow/test_carddata.py
@@ -10,7 +10,7 @@ from wcs.fields import DateField, FileField, ItemField, MapField, StringField
 from wcs.formdef import FormDef
 from wcs.qommon.http_request import HTTPRequest
 from wcs.qommon.upload_storage import PicklableUpload
-from wcs.wf.create_formdata import Mapping
+from wcs.wf.create_formdata import JournalAssignationErrorPart, Mapping
 from wcs.workflows import Workflow
 
 from ..utilities import clean_temporary_pub, create_temporary_pub
@@ -505,11 +505,14 @@ def test_create_carddata_user_association(pub):
 
         assert carddef.data_class().count() == 1
         assert carddef.data_class().select()[0].user is None
+        assert isinstance(formdata.evolution[1].parts[0], JournalAssignationErrorPart)
+        assert formdata.evolution[1].parts[0].label == 'Create Card Data (My card)'
+        assert (
+            formdata.evolution[1].parts[0].summary
+            == 'Failed to attach user (not found: "%s")' % invalid_user[1]
+        )
         if pub.loggederror_class:
-            assert pub.loggederror_class.count() == 1
-            logged_error = pub.loggederror_class.select()[0]
-            assert logged_error.summary == 'Failed to attach user (not found: "%s")' % invalid_user[1]
-            assert logged_error.formdata_id == str(formdata.id)
+            assert pub.loggederror_class.count() == 0  # no logged error
 
     # user association on invalid template
     carddef.data_class().wipe()
@@ -1546,11 +1549,11 @@ def test_assign_carddata_user_association(pub):
     formdata.perform_workflow()
 
     assert carddef.data_class().select()[0].user is None
+    assert isinstance(formdata.evolution[1].parts[0], JournalAssignationErrorPart)
+    assert formdata.evolution[1].parts[0].label == 'Assign Card Data (Person)'
+    assert formdata.evolution[1].parts[0].summary == 'Failed to attach user (not found: "zzz")'
     if pub.loggederror_class:
-        assert pub.loggederror_class.count() == 1
-        logged_error = pub.loggederror_class.select()[0]
-        assert logged_error.summary == 'Failed to attach user (not found: "zzz")'
-        assert logged_error.formdata_id == str(formdata.id)
+        assert pub.loggederror_class.count() == 0  # no logged error
 
     # user association on invalid template
     carddata.user_id = user2.id
diff --git a/wcs/wf/create_formdata.py b/wcs/wf/create_formdata.py
index 9964cf4d6..511b2ff87 100644
--- a/wcs/wf/create_formdata.py
+++ b/wcs/wf/create_formdata.py
@@ -19,7 +19,7 @@ import xml.etree.ElementTree as ET
 
 from django.utils.functional import cached_property
 from quixote import get_publisher, get_request, get_session
-from quixote.html import htmltext
+from quixote.html import TemplateIO, htmltext
 
 from wcs.formdef import FormDef
 from wcs.qommon import _, ngettext
@@ -129,6 +129,44 @@ class MappingsWidget(WidgetListAsTable):
             self.value.sort(key=lambda mapping: self.ranks.get(str(mapping.field_id), 9999))
 
 
+class JournalAssignationErrorPart(EvolutionPart):
+    summary = None
+    label = None
+    render_for_fts = None
+
+    def __init__(self, summary, label=None):
+        self.summary = summary
+        self.label = label
+
+    def is_hidden(self):
+        return not (get_request() and get_request().get_path().startswith('/backoffice/'))
+
+    def view(self):
+        r = TemplateIO(html=True)
+        r += htmltext('<div class="assignation-error">')
+        r += htmltext('<h4 class="foldable folded">')
+        r += str(_('Assignation error during action "%s"') % self.label)
+        r += htmltext('</h4>')
+        r += htmltext('<div>')
+        r += htmltext('<p>%s</p>\n') % self.summary
+        r += htmltext('</div>')
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def get_json_export_dict(self, anonymise=False, include_files=True):
+        d = {
+            'type': 'assignation-error',
+        }
+        if not anonymise:
+            d.update(
+                {
+                    'summary': self.summary,
+                    'label': self.label,
+                }
+            )
+        return d
+
+
 class LinkedFormdataEvolutionPart(EvolutionPart):
     formdef_class = FormDef
     attach_to_history = False
@@ -539,11 +577,13 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
             else:
                 dest.user = get_publisher().user_class.lookup_by_string(str(value))
                 if value and not dest.user:
-                    get_publisher().record_error(
-                        _('Failed to attach user (not found: "%s")') % value,
-                        formdata=src,
-                        status_item=self,
+                    src.evolution[-1].add_part(
+                        JournalAssignationErrorPart(
+                            _('Failed to attach user (not found: "%s")') % value,
+                            "%s (%s)" % (self.description, self.formdef.name),
+                        )
                     )
+                    src.store()
         else:
             dest.user_id = None
 
-- 
2.35.1

