From 9cd83cb2428a7b0b3ae4c54ba0203e39ee6e61ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 31 Jul 2020 11:28:02 +0200
Subject: [PATCH] backoffice: list related forms and cards in inspect view
 (#43359)

---
 tests/test_backoffice_pages.py | 231 ++++++++++++++++++++++++++++++++-
 wcs/backoffice/management.py   |  29 ++++-
 wcs/formdata.py                |  50 +++++++
 wcs/wf/external_workflow.py    |  32 +----
 4 files changed, 299 insertions(+), 43 deletions(-)

diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index e0d2c72e..08f847a5 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -3,9 +3,7 @@ import datetime
 import json
 import os
 import re
-import shutil
 import time
-import hashlib
 import random
 import xml.etree.ElementTree as ET
 import zipfile
@@ -18,24 +16,21 @@ try:
 except ImportError:
     xlwt = None
 
-from django.utils import six
 from django.utils.six import StringIO, BytesIO
 from django.utils.six.moves.urllib import parse as urllib
 from django.utils.six.moves.urllib import parse as urlparse
 
 from webtest import Upload
 
-from quixote import cleanup, get_publisher, get_response
+from quixote import get_publisher
 from quixote.http_request import Upload as QuixoteUpload
 from wcs.qommon import ods
 from wcs.api_utils import sign_url
 from wcs.blocks import BlockDef
-from wcs.qommon import errors, sessions
 from wcs.qommon.form import PicklableUpload
 from wcs.qommon.form import UploadedFile
 from wcs.qommon.ident.password_accounts import PasswordAccount
 from wcs.qommon.http_request import HTTPRequest
-from wcs.qommon.substitution import CompatibilityNamesDict
 from wcs.roles import Role, logged_users_role
 from wcs.workflows import (Workflow, CommentableWorkflowStatusItem,
         ChoiceWorkflowStatusItem, EditableWorkflowStatusItem,
@@ -5261,6 +5256,10 @@ def local_user():
 
 
 def test_inspect_page(pub, local_user):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     create_user(pub)
     create_environment(pub)
 
@@ -5417,7 +5416,219 @@ def test_inspect_page(pub, local_user):
     assert 'Invalid block tag' in resp.text
 
 
+def test_inspect_page_with_related_objects(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
+    user = create_user(pub, is_admin=True)
+
+    FormDef.wipe()
+    CardDef.wipe()
+
+    # test ExternalWorkflowGlobalAction
+    external_wf = Workflow(name='External Workflow')
+    st1 = external_wf.add_status(name='New')
+    action = external_wf.add_global_action('Delete', 'delete')
+    action.append_item('remove')
+    trigger = action.append_trigger('webservice')
+    trigger.identifier = 'delete'
+    external_wf.store()
+
+    external_formdef = FormDef()
+    external_formdef.name = 'External Form'
+    external_formdef.fields = [
+        fields.StringField(id='0', label='string', varname='form_string'),
+    ]
+    external_formdef.workflow = external_wf
+    external_formdef.store()
+
+    external_carddef = CardDef()
+    external_carddef.name = 'External Card'
+    external_carddef.fields = [
+        fields.StringField(id='0', label='string', varname='card_string'),
+    ]
+    external_carddef.backoffice_submission_roles = user.roles
+    external_carddef.workflow = external_wf
+    external_carddef.store()
+
+    wf = Workflow(name='External actions')
+    st1 = wf.add_status('Create external formdata')
+    create_formdata = CreateFormdataWorkflowStatusItem()
+    create_formdata.label = 'create linked form'
+    create_formdata.formdef_slug = external_formdef.url_name
+    create_formdata.varname = 'created_form'
+    create_formdata.id = '_create_form'
+    mappings = [
+        Mapping(field_id='0', expression='{{ form_var_string }}')
+    ]
+    create_formdata.mappings = mappings
+    create_formdata.parent = st1
+
+    create_carddata = CreateCarddataWorkflowStatusItem()
+    create_carddata.label = 'create linked card'
+    create_carddata.formdef_slug = external_carddef.url_name
+    create_carddata.varname = 'created_card'
+    create_carddata.id = '_create_card'
+    create_carddata.mappings = mappings
+    create_carddata.parent = st1
+
+    st1.items.append(create_formdata)
+    st1.items.append(create_carddata)
+
+    global_action = wf.add_global_action('Delete external linked object', 'delete')
+    action = global_action.append_item('external_workflow_global_action')
+    action.slug = 'formdef:%s' % external_formdef.url_name
+    action.trigger_id = 'action:%s' % trigger.identifier
+    wf.store()
+
+    formdef = FormDef()
+    formdef.name = 'External action form'
+    formdef.fields = [
+        fields.StringField(id='0', label='string', varname='string'),
+    ]
+    formdef.workflow = wf
+    formdef.store()
+
+    carddef = CardDef()
+    carddef.name = 'External action card'
+    carddef.fields = [
+        fields.StringField(id='0', label='string', varname='string'),
+    ]
+    carddef.backoffice_submission_roles = user.roles
+    carddef.workflow = wf
+    carddef.store()
+
+    assert formdef.data_class().count() == 0
+    assert carddef.data_class().count() == 0
+    assert external_formdef.data_class().count() == 0
+    assert external_carddef.data_class().count() == 0
+
+    formdata = formdef.data_class()()
+    formdata.data = {'0': 'test form'}
+    formdata.user = user
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert formdef.data_class().count() == 1
+    assert carddef.data_class().count() == 0
+    # related form and card
+    assert external_formdef.data_class().count() == 1
+    assert external_carddef.data_class().count() == 1
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/external-action-form/1/inspect')
+    assert 'Related Forms/Cards' in resp.text
+    # related form and card
+    assert '<li><a href="http://example.net/backoffice/management/external-form/1/">External Form #1-1</a></li>' in resp.text
+    assert '<li><a href="http://example.net/backoffice/data/external-card/1/">External Card #1-1</a></li>' in resp.text
+    # check related form
+    resp = app.get('/backoffice/management/external-form/1/')
+    assert '<h3>Original form</h3><p><a href="http://example.net/backoffice/management/external-action-form/1/">External action form #2-1</a></p>' in resp.text
+    resp = app.get('/backoffice/management/external-form/1/inspect')
+    # parent
+    assert '<li><a href="http://example.net/backoffice/management/external-action-form/1/">External action form #2-1</a></li>' in resp.text
+    # check related card
+    resp = app.get('/backoffice/data/external-card/1/')
+    assert '<h3>Original form</h3><p><a href="http://example.net/backoffice/management/external-action-form/1/">External action form #2-1</a></p>' in resp.text
+    resp = app.get('/backoffice/data/external-card/1/inspect')
+    # parent
+    assert '<li><a href="http://example.net/backoffice/management/external-action-form/1/">External action form #2-1</a></li>' in resp.text
+
+    formdef.data_class().wipe()
+    carddef.data_class().wipe()
+    external_formdef.data_class().wipe()
+    external_carddef.data_class().wipe()
+
+    carddata = carddef.data_class()()
+    carddata.data = {'0': 'test card'}
+    carddata.user = user
+    carddata.store()
+    carddata.just_created()
+    carddata.perform_workflow()
+
+    assert formdef.data_class().count() == 0
+    assert carddef.data_class().count() == 1
+    # related form and card
+    assert external_formdef.data_class().count() == 1
+    assert external_carddef.data_class().count() == 1
+
+    resp = app.get('/backoffice/data/external-action-card/1/inspect')
+    assert 'Related Forms/Cards' in resp.text
+    # related form and card
+    assert '<li><a href="http://example.net/backoffice/management/external-form/2/">External Form #1-2</a></li>' in resp.text
+    assert '<li><a href="http://example.net/backoffice/data/external-card/2/">External Card #1-2</a></li>' in resp.text
+    # check related form
+    resp = app.get('/backoffice/management/external-form/2/')
+    assert '<h3>Original card</h3><p><a href="http://example.net/backoffice/data/external-action-card/1/">External action card #2-1</a></p>' in resp.text
+    resp = app.get('/backoffice/management/external-form/2/inspect')
+    # parent
+    assert '<li><a href="http://example.net/backoffice/data/external-action-card/1/">External action card #2-1</a></li>' in resp.text
+    # check related card
+    resp = app.get('/backoffice/data/external-card/2/')
+    assert '<h3>Original card</h3><p><a href="http://example.net/backoffice/data/external-action-card/1/">External action card #2-1</a></p>' in resp.text
+    resp = app.get('/backoffice/data/external-card/2/inspect')
+    # parent
+    assert '<li><a href="http://example.net/backoffice/data/external-action-card/1/">External action card #2-1</a></li>' in resp.text
+
+    FormDef.wipe()
+    CardDef.wipe()
+    Workflow.wipe()
+
+    # test linked Card in datasource
+    wf = Workflow(name='WF')
+    st1 = wf.add_status('NEW')
+    wf.store()
+    wfc = Workflow(name='WFC')
+    wfc.add_status('NEW')
+    wfc.store()
+
+    carddef = CardDef()
+    carddef.name = 'CARD A'
+    carddef.fields = [
+        fields.StringField(id='0', label='string', varname='string'),
+    ]
+    carddef.backoffice_submission_roles = user.roles
+    carddef.workflow = wfc
+    carddef.store()
+
+    datasource = {'type': 'carddef:%s' % carddef.url_name}
+    formdef = FormDef()
+    formdef.name = 'FORM A'
+    formdef.fields = [
+        fields.ItemField(
+            id='0', label='Card',
+            type='item', varname='card',
+            data_source=datasource),
+    ]
+    formdef.workflow = wf
+    formdef.store()
+
+    carddata = carddef.data_class()()
+    carddata.data = {'0': 'Text'}
+    carddata.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {'0': '1'}
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert formdef.data_class().count() == 1
+    assert carddef.data_class().count() == 1
+
+    resp = app.get('/backoffice/management/form-a/1/inspect')
+    assert 'Related Forms/Cards' in resp.text
+    # related card
+    assert '<li><a href="http://example.net/backoffice/data/card-a/1/">CARD A #1-1</a></li>' in resp.text
+
+
 def test_workflow_jump_previous(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     user = create_user(pub)
     create_environment(pub)
 
@@ -6600,6 +6811,10 @@ def test_lazy_eval_with_conditional_workflow_form(pub):
 
 @pytest.fixture(params=[{'attach_to_history': True}, {}])
 def create_formdata(request, pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     admin = create_user(pub, is_admin=True)
 
     FormDef.wipe()
@@ -6857,6 +7072,10 @@ def test_backoffice_create_formdata_map_fields_by_varname(create_formdata):
 
 
 def test_backoffice_create_carddata_from_formdata(pub, studio):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     CardDef.wipe()
     FormDef.wipe()
 
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 933b2bec..a408f70f 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -2740,17 +2740,23 @@ class FormBackOfficeStatusPage(FormStatusPage):
             extra_context = formdata.submission_context or {}
             r += htmltext('<div class="extra-context">')
             if extra_context.get('orig_formdef_id'):
-                r += htmltext('<h3>%s</h3>') % _('Original form')
+                object_type = extra_context.get('orig_object_type', 'formdef')
+                if object_type == 'formdef':
+                    r += htmltext('<h3>%s</h3>') % _('Original form')
+                    object_class = FormDef
+                else:
+                    r += htmltext('<h3>%s</h3>') % _('Original card')
+                    object_class = CardDef
                 try:
-                    orig_formdata = FormDef.get(extra_context.get('orig_formdef_id')
-                            ).data_class().get(extra_context.get('orig_formdata_id'))
+                    orig_formdata = object_class.get(
+                        extra_context.get('orig_formdef_id')
+                    ).data_class().get(extra_context.get('orig_formdata_id'))
                 except KeyError:
                     r += htmltext('<p>%s</p>') % _('(deleted)')
                 else:
-                    r += htmltext('<p><a href="%s">%s %s</a></p>') % (
+                    r += htmltext('<p><a href="%s">%s</a></p>') % (
                              orig_formdata.get_url(backoffice=True),
-                             orig_formdata.formdef.name,
-                             orig_formdata.get_display_id())
+                             orig_formdata.get_display_name())
             if formdata.submission_channel:
                 r += htmltext('<h3>%s</h3>') % '%s: %s' % (
                         _('Channel'), formdata.get_submission_channel_label())
@@ -3141,6 +3147,17 @@ class FormBackOfficeStatusPage(FormStatusPage):
             r += htmltext('</ul>')
             r += htmltext('</div>')
 
+        children = list(self.filled.iter_target_datas())
+        if children:
+            r += htmltext('<div id="inspect-related" class="section">')
+            r += htmltext('<h2>%s</h2>') % _('Related Forms/Cards')
+            r += htmltext('<ul class="form-inspector biglist">')
+            for child in children:
+                r += htmltext('<li><a href="%s">%s</a></li>') % (
+                    child.get_url(backoffice=True), child.get_display_name())
+            r += htmltext('</ul>')
+            r += htmltext('</div>')
+
         return r.getvalue()
 
     def inspect_tool(self):
diff --git a/wcs/formdata.py b/wcs/formdata.py
index f240af28..22205fbb 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -1195,6 +1195,56 @@ class FormData(StorableObject):
             for part in evo.parts or []:
                 yield part
 
+    def iter_target_datas(self, objectdef=None, slug=None, status_item=None):
+        # objectdef, slug and status_item are provided when called from ExternalWorkflowGlobalAction
+        from wcs.wf.create_formdata import LinkedFormdataEvolutionPart
+        from wcs.logged_errors import LoggedError
+        from .carddef import CardDef
+        from .formdef import FormDef
+
+        parent = self.get_parent()
+        if parent:
+            parent_identifier = '%s:%s' % (parent.formdef.xml_root_node, parent.formdef.url_name)
+            if not objectdef or parent_identifier == slug:
+                yield parent
+
+        data_ids = []
+        # search linked objects in data sources
+        for field in self.get_formdef().get_all_fields():
+            if getattr(field, 'data_source', None) and (not objectdef or field.data_source['type'] == slug):
+                linked_id = self.data.get(field.id)
+                if linked_id:
+                    data_ids.append((field.data_source['type'], linked_id))
+
+        # search in evolution
+        for part in self.iter_evolution_parts():
+            if isinstance(part, LinkedFormdataEvolutionPart):
+                part_identifier = '%s:%s' % (part.formdef.xml_root_node, part.formdef.url_name)
+                if not objectdef or part_identifier == slug:
+                    data_ids.append((part_identifier, part.formdata_id))
+
+        for (slug, target_id) in data_ids:
+            if objectdef:
+                try:
+                    yield objectdef.data_class().get(target_id)
+                except KeyError as e:
+                    # use custom error message depending on target type
+                    LoggedError.record(_('Could not find linked "%(object_name)s" object by id %(object_id)s') % {
+                        'object_name': objectdef.name, 'object_id': target_id},
+                        formdata=self, status_item=self, exception=e)
+            else:
+                try:
+                    object_type, slug = slug.split(':')
+                    if object_type == 'formdef':
+                        object_class = FormDef
+                    elif object_type == 'carddef':
+                        object_class = CardDef
+                    yield object_class.get_by_urlname(slug).data_class().get(target_id)
+                except ValueError:
+                    pass
+                except KeyError:
+                    pass
+
     def __getattr__(self, attr):
         try:
             return self.__dict__[attr]
diff --git a/wcs/wf/external_workflow.py b/wcs/wf/external_workflow.py
index 270d8209..23749e8f 100644
--- a/wcs/wf/external_workflow.py
+++ b/wcs/wf/external_workflow.py
@@ -22,7 +22,6 @@ from wcs.qommon.form import SingleSelectWidget
 from wcs.logged_errors import LoggedError
 from wcs.workflows import WorkflowStatusItem, perform_items, register_item_class
 from wcs.workflows import WorkflowGlobalActionWebserviceTrigger, Workflow
-from wcs.wf.create_formdata import LinkedFormdataEvolutionPart
 from wcs.carddef import CardDef
 from wcs.formdef import FormDef
 
@@ -115,36 +114,7 @@ class ExternalWorkflowGlobalAction(WorkflowStatusItem):
         return _('not completed')
 
     def iter_target_datas(self, formdata, objectdef):
-        parent = formdata.get_parent()
-        if parent:
-            parent_identifier = '%s:%s' % (parent.formdef.xml_root_node,
-                    parent.formdef.url_name)
-            if parent_identifier == self.slug:
-                yield parent
-
-        data_ids = []
-        # search linked objects in data sources
-        for field in formdata.get_formdef().get_all_fields():
-            if getattr(field, 'data_source', None) and field.data_source['type'] == self.slug:
-                linked_id = formdata.data.get(field.id)
-                if linked_id:
-                    data_ids.append(linked_id)
-
-        # search in evolution
-        for part in formdata.iter_evolution_parts():
-            if isinstance(part, LinkedFormdataEvolutionPart):
-                part_identifier = '%s:%s' % (part.formdef.xml_root_node, part.formdef.url_name)
-                if part_identifier == self.slug:
-                    data_ids.append(part.formdata_id)
-
-        for target_id in data_ids:
-            try:
-                yield objectdef.data_class().get(target_id)
-            except KeyError as e:
-                # use custom error message depending on target type
-                LoggedError.record(_('Could not find linked "%(object_name)s" object by id %(object_id)s') % {
-                    'object_name': objectdef.name, 'object_id': target_id},
-                    formdata=formdata, status_item=self, exception=e)
+        yield from formdata.iter_target_datas(objectdef=objectdef, slug=self.slug, status_item=self)
 
     def get_parameters(self):
         return ('slug', 'trigger_id', 'condition')
-- 
2.28.0

