From e768098083d370f8eb7ef5b5ce07fa652fd5aee7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Wed, 18 May 2022 22:00:01 +0200
Subject: [PATCH] backoffice: add button/link to jump to selected option admin
 page (#65681)

---
 tests/admin_pages/test_form.py       |  8 ++++++++
 tests/admin_pages/test_workflow.py   | 13 +++++++++++--
 tests/test_fields.py                 |  1 +
 wcs/data_sources.py                  | 17 ++++++++++++++++-
 wcs/qommon/static/css/dc2/admin.scss |  7 +++++++
 wcs/qommon/static/js/qommon.admin.js | 17 +++++++++++++++++
 wcs/wf/create_formdata.py            |  7 +++++--
 wcs/wf/editable.py                   |  6 +++++-
 wcs/wf/external_workflow.py          | 10 ++++++++--
 wcs/wf/wscall.py                     |  9 +++++++--
 wcs/workflows.py                     |  9 ++++++---
 11 files changed, 91 insertions(+), 13 deletions(-)

diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py
index 62d5362b3..9ede07b6c 100644
--- a/tests/admin_pages/test_form.py
+++ b/tests/admin_pages/test_form.py
@@ -2154,6 +2154,14 @@ def test_form_edit_item_field_data_source(pub):
         ('jsonp', False, 'JSONP URL'),
         ('python', False, 'Python Expression'),
     ]
+    assert (
+        resp.pyquery('select[id="form_data_source$type"] option')[1].attrib['data-goto-url']
+        == carddef.get_admin_url()
+    )
+    assert (
+        resp.pyquery('select[id="form_data_source$type"] option')[2].attrib['data-goto-url']
+        == data_source.get_admin_url()
+    )
 
     resp.form['data_source$type'].value = 'carddef:%s' % carddef.url_name
     resp = resp.form.submit('submit').follow()
diff --git a/tests/admin_pages/test_workflow.py b/tests/admin_pages/test_workflow.py
index 9434cfda1..58c7b0858 100644
--- a/tests/admin_pages/test_workflow.py
+++ b/tests/admin_pages/test_workflow.py
@@ -1432,7 +1432,7 @@ def test_workflows_edit_choice_action(pub):
     create_superuser(pub)
     Workflow.wipe()
     workflow = Workflow(name='foo')
-    workflow.add_status(name='baz')
+    baz_status = workflow.add_status(name='baz')
     workflow.store()
 
     app = login(get_app(pub))
@@ -1444,8 +1444,12 @@ def test_workflows_edit_choice_action(pub):
     resp = resp.follow()
 
     resp = resp.click(href='items/1/', index=0)
+    assert (
+        resp.pyquery('select#form_status option')[1].attrib['data-goto-url']
+        == 'http://example.net/backoffice/workflows/1/status/1/'
+    )
     assert 'Previously Marked Status' not in [x[2] for x in resp.form['status'].options]
-    resp.form['status'].value = 'baz'
+    resp.form['status'].value = baz_status.id
     resp = resp.form.submit('submit')
     resp = resp.follow()
     resp = resp.follow()
@@ -1457,6 +1461,7 @@ def test_workflows_edit_choice_action(pub):
     resp = resp.follow()
 
     resp = resp.click(href='items/1/', index=0)
+    assert 'data-goto-url' not in resp.pyquery('select#form_status option')[2].attrib
     assert 'Previously Marked Status' in [x[2] for x in resp.form['status'].options]
 
 
@@ -2446,6 +2451,10 @@ def test_workflows_global_actions_external_workflow_action(pub):
     resp = resp.forms[0].submit('submit')
     assert "required field" in resp.text
     resp.forms[0]['slug'] = 'formdef:%s' % formdef.url_name
+    assert (
+        resp.pyquery('select#form_slug option')[1].attrib['data-goto-url']
+        == 'http://example.net/backoffice/forms/1/'
+    )
     resp = resp.forms[0].submit('submit')
     assert "required field" in resp.text
     resp = resp.forms[0].submit('submit')
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 8dbd753d5..3606cba70 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -33,6 +33,7 @@ def teardown_module(module):
 
 
 def test_fill_admin_form(pub):
+    pub.root_directory.backoffice = pub.backoffice_directory_class()
     for klass in fields.field_classes:
         form = Form(use_tokens=False)
         klass().fill_admin_form(form)
diff --git a/wcs/data_sources.py b/wcs/data_sources.py
index 33cd65843..7a25e5890 100644
--- a/wcs/data_sources.py
+++ b/wcs/data_sources.py
@@ -67,12 +67,18 @@ class DataSourceSelectionWidget(CompositeWidget):
         if allow_named_sources:
             from wcs.carddef import CardDef
 
-            cards_options = [(t[2], t[1], t[2]) for t in CardDef.get_carddefs_as_data_source()]
+            cards_options = [
+                (t[2], t[1], t[2], {'data-goto-url': t[0].get_admin_url()})
+                for t in CardDef.get_carddefs_as_data_source()
+            ]
+            if not get_publisher().get_backoffice_root().is_accessible('cards'):
+                cards_options = [x[:3] for x in cards_options]
             cards_options.sort(key=lambda x: misc.simplify(x[1]))
             if cards_options:
                 options.append(OptGroup(_('Cards')))
                 options.extend(cards_options)
 
+            admin_accessible = NamedDataSource.is_admin_accessible()
             nds_options = []
             nds_agenda_options = []
             nds_users_options = []
@@ -86,6 +92,8 @@ class DataSourceSelectionWidget(CompositeWidget):
                         'data-maybe-datetimes': 'true' if ds.maybe_datetimes() else 'false',
                     },
                 ]
+                if admin_accessible:
+                    option[-1]['data-goto-url'] = ds.get_admin_url()
                 if ds.external == 'agenda':
                     nds_agenda_options.append(option)
                 elif ds.type == 'wcs:users':
@@ -595,6 +603,13 @@ class NamedDataSource(XmlStorableObject):
                 instance=self, comment=comment, store_user=snapshot_store_user
             )
 
+    @classmethod
+    def is_admin_accessible(cls):
+        for section in ('settings', 'forms', 'workflows'):
+            if get_publisher().get_backoffice_root().is_accessible(section):
+                return True
+        return False
+
     def get_admin_url(self):
         base_url = get_publisher().get_backoffice_url()
         if get_request():
diff --git a/wcs/qommon/static/css/dc2/admin.scss b/wcs/qommon/static/css/dc2/admin.scss
index 866f29a2d..bf1fd28f7 100644
--- a/wcs/qommon/static/css/dc2/admin.scss
+++ b/wcs/qommon/static/css/dc2/admin.scss
@@ -2442,3 +2442,10 @@ table.diff {
 		margin-top: 0;
 	}
 }
+
+a.button.button--go-to-option {
+	margin-left: 2px;
+	display: inline;
+	padding-left: 10px;
+	padding-right: 10px;
+}
diff --git a/wcs/qommon/static/js/qommon.admin.js b/wcs/qommon/static/js/qommon.admin.js
index 886b64d3a..2a5a53a0a 100644
--- a/wcs/qommon/static/js/qommon.admin.js
+++ b/wcs/qommon/static/js/qommon.admin.js
@@ -7,6 +7,23 @@ $(function() {
     });
     $('h4.foldable.folded').next().hide();
 
+    $('option[data-goto-url]').parents('select').on('change', function() {
+      var jumpto_button = this.jumpto_button;
+      if (typeof jumpto_button == 'undefined') {
+        this.jumpto_button = $('<a>', {'class': 'button button--go-to-option', text: '↗'});
+        this.jumpto_button.hide();
+        this.jumpto_button.insertAfter($(this));
+      }
+      var option = $(this).find('option:selected');
+      var data_url = option.data('goto-url');
+      if (data_url) {
+        this.jumpto_button.attr('href', data_url);
+        this.jumpto_button.show();
+      } else {
+        this.jumpto_button.hide();
+      }
+    }).trigger('change');
+
     /* focus tab from #open:<tab slug> anchor, used in documentation links
      * to point to open panel */
     if (document.location.hash && document.location.hash.indexOf('#open:') == 0) {
diff --git a/wcs/wf/create_formdata.py b/wcs/wf/create_formdata.py
index 9c4eba534..a5943f645 100644
--- a/wcs/wf/create_formdata.py
+++ b/wcs/wf/create_formdata.py
@@ -343,12 +343,15 @@ class CreateFormdataWorkflowStatusItem(WorkflowStatusItem):
     def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
         super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
         if 'formdef_slug' in parameters:
-            list_forms = [(None, '---', '')]
+            list_forms = [(None, '---', '', {})]
             list_forms += [
-                (x.url_name, x.name, x.url_name)
+                (x.url_name, x.name, x.url_name, {'data-goto-url': x.get_admin_url()})
                 for x in self.formdef_class.select(order_by='name')
                 if not x.disabled or x.url_name == self.formdef_slug
             ]
+            if not get_publisher().get_backoffice_root().is_accessible(self.formdef_class.backoffice_section):
+                # do not include goto url if section is not accessible
+                list_forms = [(x[0], x[1], x[2], {}) for x in list_forms]
             form.add(
                 SingleSelectWidget,
                 '%sformdef_slug' % prefix,
diff --git a/wcs/wf/editable.py b/wcs/wf/editable.py
index 622540b96..89d86bb2e 100644
--- a/wcs/wf/editable.py
+++ b/wcs/wf/editable.py
@@ -75,7 +75,11 @@ class EditableWorkflowStatusItem(WorkflowStatusItem):
                 title=_('Status After Edit'),
                 value=self.status,
                 hint=_("Don't select any if you don't want status change processing"),
-                options=[(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status],
+                options=[(None, '---', '', {})]
+                + [
+                    (x.id, x.name, x.id, {'data-goto-url': x.get_admin_url()})
+                    for x in self.parent.parent.possible_status
+                ],
             )
         if 'label' in parameters:
             form.add(StringWidget, '%slabel' % prefix, title=_('Button Label'), value=self.label)
diff --git a/wcs/wf/external_workflow.py b/wcs/wf/external_workflow.py
index 035270c26..a86bab095 100644
--- a/wcs/wf/external_workflow.py
+++ b/wcs/wf/external_workflow.py
@@ -109,12 +109,18 @@ class ExternalWorkflowGlobalAction(WorkflowStatusItem):
         super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
 
         if 'slug' in parameters:
-            objects = [(None, '---', '')]
+            objects = [(None, '---', '', {})]
+            is_admin_accessible = {
+                'forms': get_publisher().get_backoffice_root().is_accessible('forms'),
+                'cards': get_publisher().get_backoffice_root().is_accessible('cards'),
+            }
             for wf in Workflow.select():
                 if any(self.get_workflow_webservice_triggers(wf)):
                     for objectdef in wf.formdefs(lightweight=True) + wf.carddefs(lightweight=True):
                         object_slug = '%s:%s' % (objectdef.__class__.__name__.lower(), objectdef.url_name)
-                        objects.append((object_slug, objectdef.name, object_slug))
+                        objects.append((object_slug, objectdef.name, object_slug, {}))
+                        if is_admin_accessible[objectdef.backoffice_section]:
+                            objects[-1][-1]['data-goto-url'] = objectdef.get_admin_url()
             if len(objects) == 1:
                 form.add_global_errors([_('No workflow with external triggerable global action.')])
                 return
diff --git a/wcs/wf/wscall.py b/wcs/wf/wscall.py
index ac5db7b39..63265ee0a 100644
--- a/wcs/wf/wscall.py
+++ b/wcs/wf/wscall.py
@@ -309,8 +309,13 @@ class WebserviceCallStatusItem(WorkflowStatusItem):
                     tab=('response', _('Response')),
                 )
 
-        error_actions = [(':stop', _('Stop')), (':pass', _('Ignore'))]
-        error_actions.extend([(x.id, _('Jump to %s') % x.name) for x in self.parent.parent.possible_status])
+        error_actions = [(':stop', _('Stop'), ':stop', {}), (':pass', _('Ignore'), ':pass', {})]
+        error_actions.extend(
+            [
+                (x.id, _('Jump to %s') % x.name, str(x.id), {'data-goto-url': x.get_admin_url()})
+                for x in self.parent.parent.possible_status
+            ]
+        )
         for attribute in (
             'action_on_app_error',
             'action_on_4xx',
diff --git a/wcs/workflows.py b/wcs/workflows.py
index abed7cc1d..6a42d0d6b 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -2661,7 +2661,10 @@ class WorkflowStatusJumpItem(WorkflowStatusItem):
     def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
         super().add_parameters_widgets(form, parameters, prefix=prefix, formdef=formdef, **kwargs)
         if 'status' in parameters:
-            destinations = [(x.id, x.name) for x in self.parent.parent.possible_status]
+            destinations = [
+                (x.id, x.name, x.id, {'data-goto-url': x.get_admin_url()})
+                for x in self.parent.parent.possible_status
+            ]
 
             # look for existing jumps that are dropping a mark
             workflow = self.parent.parent
@@ -2670,7 +2673,7 @@ class WorkflowStatusJumpItem(WorkflowStatusItem):
             for status in statuses + global_actions:
                 for item in status.items:
                     if getattr(item, 'set_marker_on_status', False):
-                        destinations.append(('_previous', _('Previously Marked Status')))
+                        destinations.append(('_previous', _('Previously Marked Status'), '_previous', {}))
                         break
                 else:
                     continue
@@ -2681,7 +2684,7 @@ class WorkflowStatusJumpItem(WorkflowStatusItem):
                 '%sstatus' % prefix,
                 title=_('Status'),
                 value=self.status,
-                options=[(None, '---')] + destinations,
+                options=[(None, '---', '', {})] + destinations,
             )
 
         if 'set_marker_on_status' in parameters:
-- 
2.36.1

