From b07fef8ba2d801fc567289d836f8b53d9de28325 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 4 Nov 2021 14:50:26 +0100
Subject: [PATCH 4/4] workflows: report all missing things in one time (#57753)

---
 tests/test_workflow_import.py | 78 ++++++++++++++++++++++++++++++++++-
 wcs/formdef.py                | 25 +++++++++--
 wcs/workflows.py              | 74 ++++++++++++++++++++++++++++-----
 3 files changed, 161 insertions(+), 16 deletions(-)

diff --git a/tests/test_workflow_import.py b/tests/test_workflow_import.py
index a2209414..5d7f302b 100644
--- a/tests/test_workflow_import.py
+++ b/tests/test_workflow_import.py
@@ -136,8 +136,10 @@ def test_action_dispatch(pub):
     pub.cfg['sp'] = {'idp-manage-roles': True}
     # now roles are managed: cannot create them
     dispatch.role_id = 'unknown'
-    with pytest.raises(WorkflowImportError, match=r'.*Unknown referenced role.*'):
+    with pytest.raises(WorkflowImportError) as excinfo:
         wf2 = assert_import_export_works(wf)
+    assert excinfo.value.msg == 'Unknown referenced objects'
+    assert excinfo.value.details == 'Unknown roles: unknown'
     # but allow computed roles
     dispatch.role_id = '=form_var_bar'
     wf2 = assert_import_export_works(wf)
@@ -895,8 +897,10 @@ def test_worklow_with_mail_template(pub):
     # import with non existing mail template
     MailTemplate.wipe()
     export = ET.tostring(wf.export_to_xml(include_id=True))
-    with pytest.raises(WorkflowImportError, match='Unknown referenced mail template'):
+    with pytest.raises(WorkflowImportError) as excinfo:
         Workflow.import_from_xml_tree(ET.fromstring(export), include_id=True)
+    assert excinfo.value.msg == 'Unknown referenced objects'
+    assert excinfo.value.details == 'Unknown mail templates: test-mail-template'
 
 
 def test_workflow_with_unknown_data_source(pub):
@@ -1039,3 +1043,73 @@ def test_workflow_with_category(pub):
     export = ET.tostring(wf.export_to_xml(include_id=True))
     wf3 = Workflow.import_from_xml_tree(ET.fromstring(export), include_id=True)
     assert wf3.category_id is None
+
+
+def test_import_workflow_multiple_errors(pub):
+    BlockDef.wipe()
+    pub.cfg['sp'] = {'idp-manage-roles': True}
+
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_x'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields = [
+        BlockField(id='1', type='block:foobar1'),
+        BlockField(id='2', type='block:foobaz1'),
+        StringField(id='3', type='string', data_source={'type': 'foobar1'}),
+        StringField(id='4', type='string', data_source={'type': 'carddef:unknown1'}),
+    ]
+    st1.items.append(display_form)
+    display_form.parent = st1
+
+    dispatch1 = DispatchWorkflowStatusItem()
+    dispatch1.id = '_x1'
+    dispatch1.role_id = 'unknown-role1'
+    dispatch1.role_key = 'plop'
+    st1.items.append(dispatch1)
+    dispatch1.parent = st1
+    dispatch2 = DispatchWorkflowStatusItem()
+    dispatch2.id = '_x2'
+    dispatch2.role_id = 'unknown-role2'
+    dispatch2.role_key = 'plop'
+    st1.items.append(dispatch2)
+    dispatch2.parent = st1
+
+    item1 = SendmailWorkflowStatusItem()
+    item1.to = ['_receiver']
+    item1.mail_template = 'unknown-mt-1'
+    st1.items.append(item1)
+    item1.parent = st1
+    item2 = SendmailWorkflowStatusItem()
+    item2.to = ['_receiver']
+    item2.mail_template = 'unknown-mt-2'
+    st1.items.append(item2)
+    item2.parent = st1
+
+    wf.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=wf)
+    wf.variables_formdef.fields = [
+        BlockField(id='1', type='block:foobar2'),
+        BlockField(id='2', type='block:foobaz2'),
+        StringField(id='3', type='string', data_source={'type': 'foobar2'}),
+        StringField(id='4', type='string', data_source={'type': 'carddef:unknown2'}),
+    ]
+
+    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
+    wf.backoffice_fields_formdef.fields = [
+        BlockField(id='1', type='block:foobar3'),
+        BlockField(id='2', type='block:foobaz3'),
+        StringField(id='3', type='string', data_source={'type': 'foobar3'}),
+        StringField(id='4', type='string', data_source={'type': 'carddef:unknown3'}),
+    ]
+
+    export = ET.tostring(export_to_indented_xml(wf))
+    with pytest.raises(WorkflowImportError) as excinfo:
+        Workflow.import_from_xml(io.BytesIO(export))
+    assert excinfo.value.msg == 'Unknown referenced objects'
+    assert excinfo.value.details == (
+        'Unknown datasources: carddef:unknown1, carddef:unknown2, carddef:unknown3, foobar1, foobar2, foobar3; '
+        'Unknown field types: block:foobar1, block:foobar2, block:foobar3, block:foobaz1, block:foobaz2, block:foobaz3; '
+        'Unknown mail templates: unknown-mt-1, unknown-mt-2; '
+        'Unknown roles: unknown-role1, unknown-role2'
+    )
diff --git a/wcs/formdef.py b/wcs/formdef.py
index 215b6bb4..506a43d8 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -15,6 +15,7 @@
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import base64
+import collections
 import contextlib
 import copy
 import datetime
@@ -60,6 +61,22 @@ class FormdefImportError(Exception):
         self.details = details
 
 
+class FormdefImportUnknownReferencedError(FormdefImportError):
+    def __init__(self, msg, msg_args=None, details=None):
+        self.msg = msg
+        self.msg_args = msg_args or ()
+        self._details = details
+
+    @property
+    def details(self):
+        if not self._details:
+            return None
+        details = []
+        for kind in self._details:
+            details.append('%s: %s' % (kind, ', '.join(sorted(self._details[kind]))))
+        return '; '.join(details)
+
+
 class FormdefImportRecoverableError(FormdefImportError):
     pass
 
@@ -1357,12 +1374,12 @@ class FormDef(StorableObject):
                             unknown_datasources.add(data_source_id)
 
         if unknown_field_types or unknown_datasources:
-            details = []
+            details = collections.defaultdict(set)
             if unknown_field_types:
-                details.append('%s: %s' % (_('Unknown field types'), ', '.join(sorted(unknown_field_types))))
+                details[_('Unknown field types')].update(unknown_field_types)
             if unknown_datasources:
-                details.append('%s: %s' % (_('Unknown datasources'), ', '.join(sorted(unknown_datasources))))
-            raise FormdefImportError(_('Unknown referenced objects'), details='; '.join(details))
+                details[_('Unknown datasources')].update(unknown_datasources)
+            raise FormdefImportUnknownReferencedError(_('Unknown referenced objects'), details=details)
 
         return formdef
 
diff --git a/wcs/workflows.py b/wcs/workflows.py
index aa491763..cd125296 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -34,7 +34,7 @@ from .categories import WorkflowCategory
 from .conditions import Condition
 from .fields import FileField
 from .formdata import Evolution
-from .formdef import FormDef, FormdefImportError
+from .formdef import FormDef, FormdefImportError, FormdefImportUnknownReferencedError
 from .mail_templates import MailTemplate
 from .qommon import _, emails, errors, ezt, force_str, get_cfg, misc
 from .qommon.form import (
@@ -122,6 +122,22 @@ class WorkflowImportError(Exception):
         self.details = details
 
 
+class WorkflowImportUnknownReferencedError(WorkflowImportError):
+    def __init__(self, msg, msg_args=None, details=None):
+        self.msg = msg
+        self.msg_args = msg_args or ()
+        self._details = details
+
+    @property
+    def details(self):
+        if not self._details:
+            return None
+        details = []
+        for kind in sorted(self._details.keys()):
+            details.append('%s: %s' % (kind, ', '.join(sorted(self._details[kind]))))
+        return '; '.join(details)
+
+
 class AbortActionException(Exception):
     def __init__(self, url=None):
         self.url = url
@@ -803,6 +819,7 @@ class Workflow(StorableObject):
             for role_node in tree.findall('roles/role'):
                 workflow.roles[role_node.attrib['id']] = xml_node_text(role_node)
 
+        unknown_referenced_objects_details = collections.defaultdict(set)
         workflow.possible_status = []
         for status in tree.find('possible_status'):
             status_o = WorkflowStatus()
@@ -815,9 +832,13 @@ class Workflow(StorableObject):
                     snapshot=snapshot,
                     check_datasources=check_datasources,
                 )
+            except WorkflowImportUnknownReferencedError as e:
+                for k, v in e._details.items():
+                    unknown_referenced_objects_details[k].update(v)
             except FormdefImportError as e:
                 raise WorkflowImportError(e.msg, details=e.details)
-            workflow.possible_status.append(status_o)
+            else:
+                workflow.possible_status.append(status_o)
 
         workflow.global_actions = []
         global_actions = tree.find('global_actions')
@@ -843,10 +864,14 @@ class Workflow(StorableObject):
                 imported_formdef = FormDef.import_from_xml_tree(
                     formdef, include_id=True, snapshot=snapshot, check_datasources=check_datasources
                 )
+            except FormdefImportUnknownReferencedError as e:
+                for k, v in e._details.items():
+                    unknown_referenced_objects_details[k].update(v)
             except FormdefImportError as e:
                 raise WorkflowImportError(e.msg, details=e.details)
-            workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
-            workflow.variables_formdef.fields = imported_formdef.fields
+            else:
+                workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
+                workflow.variables_formdef.fields = imported_formdef.fields
 
         variables = tree.find('backoffice-fields')
         if variables is not None:
@@ -855,10 +880,19 @@ class Workflow(StorableObject):
                 imported_formdef = FormDef.import_from_xml_tree(
                     formdef, include_id=True, snapshot=snapshot, check_datasources=check_datasources
                 )
+            except FormdefImportUnknownReferencedError as e:
+                for k, v in e._details.items():
+                    unknown_referenced_objects_details[k].update(v)
             except FormdefImportError as e:
                 raise WorkflowImportError(e.msg, details=e.details)
-            workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow=workflow)
-            workflow.backoffice_fields_formdef.fields = imported_formdef.fields
+            else:
+                workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow=workflow)
+                workflow.backoffice_fields_formdef.fields = imported_formdef.fields
+
+        if unknown_referenced_objects_details:
+            raise WorkflowImportUnknownReferencedError(
+                _('Unknown referenced objects'), details=unknown_referenced_objects_details
+            )
 
         return workflow
 
@@ -1194,7 +1228,9 @@ class XmlSerialisable:
 
         # if the roles are managed by the idp, don't try further.
         if get_publisher() and get_cfg('sp', {}).get('idp-manage-roles') is True:
-            raise WorkflowImportError(_('Unknown referenced role (%s)'), (value,))
+            raise WorkflowImportUnknownReferencedError(
+                _('Unknown referenced role (%s)'), details={_('Unknown roles'): {value}}
+            )
 
         # and if there's no match, create a new role
         role = get_publisher().role_class()
@@ -1996,13 +2032,29 @@ class WorkflowStatus:
             self.visibility.append(visibility_role.text)
 
         self.items = []
+        unknown_referenced_objects_details = collections.defaultdict(set)
         for item in elem.find('items'):
             item_type = item.attrib['type']
             self.append_item(item_type)
             item_o = self.items[-1]
             item_o.parent = self
-            item_o.init_with_xml(
-                item, charset, include_id=include_id, snapshot=snapshot, check_datasources=check_datasources
+            try:
+                item_o.init_with_xml(
+                    item,
+                    charset,
+                    include_id=include_id,
+                    snapshot=snapshot,
+                    check_datasources=check_datasources,
+                )
+            except (WorkflowImportUnknownReferencedError, FormdefImportUnknownReferencedError) as e:
+                for k, v in e._details.items():
+                    unknown_referenced_objects_details[k].update(v)
+            except FormdefImportError as e:
+                raise WorkflowImportError(e.msg, details=e.details)
+
+        if unknown_referenced_objects_details:
+            raise WorkflowImportUnknownReferencedError(
+                _('Unknown referenced objects'), details=unknown_referenced_objects_details
             )
 
     def __repr__(self):
@@ -2428,7 +2480,9 @@ class WorkflowStatusItem(XmlSerialisable):
         value = xml_node_text(elem)
         mail_template = MailTemplate.get_by_slug(value)
         if not mail_template:
-            raise WorkflowImportError(_('Unknown referenced mail template (%s)'), (value,))
+            raise WorkflowImportUnknownReferencedError(
+                _('Unknown referenced mail template (%s)'), details={_('Unknown mail templates'): {value}}
+            )
         self.mail_template = value
         return
 
-- 
2.33.0

