From c5a50999a7f570b312dab9a39b9b0f400b9d3ea0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 15 May 2020 09:43:12 +0200
Subject: [PATCH] backoffice: use a customisable template to render user info
 in sidebar (#40034)

---
 tests/test_admin_pages.py      | 53 +++++++++++++++++++++++--------
 tests/test_backoffice_pages.py | 26 ++++++++++++++++
 wcs/admin/settings.py          | 57 +++++++++++++++++++++++++++++++---
 wcs/backoffice/management.py   | 12 ++++++-
 4 files changed, 130 insertions(+), 18 deletions(-)

diff --git a/tests/test_admin_pages.py b/tests/test_admin_pages.py
index 4cd1456c0..fde9fd463 100644
--- a/tests/test_admin_pages.py
+++ b/tests/test_admin_pages.py
@@ -4727,31 +4727,31 @@ def test_settings_user(pub):
     resp = app.get('/backoffice/settings/users').follow().follow()
 
     # add a field
-    resp.forms[1]['label'] = 'foobar'
-    resp = resp.forms[1].submit()
+    resp.forms[2]['label'] = 'foobar'
+    resp = resp.forms[2].submit()
     assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
     resp = resp.follow()
     assert b'foobar' in pub.cfg['users']['formdef']
     assert 'foobar' in resp.text
 
     # set field as email
-    resp.forms[0]['field_email'] = '1'
-    resp = resp.forms[0].submit()
+    resp.forms['mapping']['field_email'] = '1'
+    resp = resp.forms['mapping'].submit()
     assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
     resp = resp.follow()
     assert pub.cfg['users']['field_email'] == '1'
 
     # and unset it
-    resp.forms[0]['field_email'] = ''
-    resp = resp.forms[0].submit()
+    resp.forms['mapping']['field_email'] = ''
+    resp = resp.forms['mapping'].submit()
     assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
     resp = resp.follow()
     assert pub.cfg['users']['field_email'] == None
 
     # add a comment field
-    resp.forms[1]['label'] = 'barfoo'
-    resp.forms[1]['type'] = 'Comment'
-    resp = resp.forms[1].submit()
+    resp.forms[2]['label'] = 'barfoo'
+    resp.forms[2]['type'] = 'Comment'
+    resp = resp.forms[2].submit()
     assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
     resp = resp.follow()
     assert b'barfoo' in pub.cfg['users']['formdef']
@@ -4760,16 +4760,43 @@ def test_settings_user(pub):
     # check fields are present in edit form
     resp = app.get('/backoffice/users/%s/edit' % user.id)
     assert 'barfoo' in resp.text
-    assert 'f1' in resp.forms[0].fields
-    assert 'email' in resp.forms[0].fields
+    assert 'f1' in resp.form.fields
+    assert 'email' in resp.form.fields
 
     # check the email field is not displayed if it's overridden by a custom
     # field.
     pub.cfg['users']['field_email'] = '1'
     pub.write_cfg()
     resp = app.get('/backoffice/users/%s/edit' % user.id)
-    assert 'f1' in resp.forms[0].fields
-    assert 'email' not in resp.forms[0].fields
+    assert 'f1' in resp.form.fields
+    assert 'email' not in resp.form.fields
+
+    # set a sidebar template
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/users').follow().follow()
+    resp.forms['template']['sidebar_template'] = 'hello {{ form_user_display_name }}'
+    resp = resp.forms['template'].submit().follow()
+    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
+    resp.forms['template']['sidebar_template'] = '{% if True %}'
+    resp = resp.forms['template'].submit().follow()
+    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
+    assert 'syntax error in Django template' in resp
+
+    # disable users screen
+    if not pub.site_options.has_section('options'):
+        pub.site_options.add_section('options')
+    pub.site_options.set('options', 'settings-disabled-screens', 'users')
+    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
+        pub.site_options.write(fd)
+
+    resp = app.get('/backoffice/settings/')
+    resp = resp.click('Users', href='user-template')
+    resp.forms['template']['sidebar_template'] = '{% if True %}'
+    resp = resp.forms['template'].submit()
+    assert 'syntax error in Django template' in resp
+    resp.forms['template']['sidebar_template'] = 'hello {{ form_user_display_name }}'
+    resp = resp.forms['template'].submit()
+    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
 
     # restore config
     pub.cfg['users']['field_email'] = None
diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index 6a5307c04..c7a611ef0 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -1913,6 +1913,32 @@ def test_backoffice_submission_context(pub):
     assert 'by %s' % user.get_display_name() in resp.text
 
 
+def test_backoffice_sidebar_user_template(pub):
+    user = create_user(pub)
+    create_environment(pub)
+    form_class = FormDef.get_by_urlname('form-title').data_class()
+    number31 = [x for x in form_class.select() if x.data['1'] == 'FOO BAR 30'][0]
+    number31.user_id = user.id
+    number31.store()
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/form-title/')
+    resp = resp.click(href='%s/' % number31.id)
+    assert 'Associated User' in resp.text
+    assert '<p>admin</p>' in resp.text
+    pub.cfg['users'] = {'sidebar_template': 'XXX{{ form_user_display_name }}YYY'}
+    pub.write_cfg()
+    resp = app.get(resp.request.url)
+    assert '<p>XXXadminYYY</p>' in resp.text
+    pub.cfg['users'] = {'sidebar_template': 'XXX<b>{{ form_user_display_name }}</b>YYY'}
+    pub.write_cfg()
+    resp = app.get(resp.request.url)
+    assert '<p>XXX<b>admin</b>YYY</p>' in resp.text
+    user.name = 'adm<i>n'
+    user.store()
+    resp = app.get(resp.request.url)
+    assert '<p>XXX<b>adm&lt;i&gt;n</b>YYY</p>' in resp.text
+
+
 def test_backoffice_geolocation_info(pub):
     user = create_user(pub)
     create_environment(pub)
diff --git a/wcs/admin/settings.py b/wcs/admin/settings.py
index 0c13dd284..7640dce74 100644
--- a/wcs/admin/settings.py
+++ b/wcs/admin/settings.py
@@ -118,7 +118,7 @@ class UserFieldDefPage(FieldDefPage):
 
 
 class UserFieldsDirectory(FieldsDirectory):
-    _q_exports = ['', 'update_order', 'new', 'mapping']
+    _q_exports = ['', 'update_order', 'new', 'mapping', 'template']
 
     section = 'settings'
     field_def_page_class = UserFieldDefPage
@@ -128,11 +128,15 @@ class UserFieldsDirectory(FieldsDirectory):
 
     def index_bottom(self):
         r = TemplateIO(html=True)
+        r += get_session().display_message()
         r += htmltext('<div class="bo-block">')
         r += htmltext('<h2>%s</h2>') % _('Fields Mapping')
         r += htmltext('<p>%s</p>') % _('These settings make it possible to assign custom user fields to standard user fields.')
-        form = self.mapping_form()
-        r += form.render()
+        r += self.mapping_form().render()
+        r += htmltext('</div>')
+        r += htmltext('<div class="bo-block">')
+        r += htmltext('<h2>%s</h2>') % _('Sidebar Template')
+        r += self.sidebar_template_form().render()
         r += htmltext('</div>')
         return r.getvalue()
 
@@ -141,7 +145,7 @@ class UserFieldsDirectory(FieldsDirectory):
         options = [(None, _('None'), '')] + [
                 (x.id, x.label, x.id) for x in self.objectdef.fields]
 
-        form = Form(action = 'mapping', enctype='multipart/form-data')
+        form = Form(action='mapping', id='mapping')
         field_name_value = users_cfg.get('field_name')
         if type(field_name_value) is str:
             field_name_value = [field_name_value]
@@ -161,6 +165,26 @@ class UserFieldsDirectory(FieldsDirectory):
         cfg_submit(form, 'users', ['field_name', 'field_email'])
         return redirect('.')
 
+    @classmethod
+    def sidebar_template_form(cls, action='template'):
+        users_cfg = get_cfg('users', {})
+        form = Form(action=action, id='template')
+        form.add(TextWidget, 'sidebar_template',
+                value=users_cfg.get('sidebar_template') or '{{ form_user_display_name }}',
+                required=False,
+                validation_function=ComputedExpressionWidget.validate_template,
+                rows=4)
+        form.add_submit('submit', _('Submit'))
+        return form
+
+    def template(self):
+        form = self.sidebar_template_form()
+        if form.has_errors():
+            get_session().message = ('error', form.get_widget('sidebar_template').get_error())
+            return redirect('.')
+        cfg_submit(form, 'users', ['sidebar_template'])
+        return redirect('.')
+
 
 class UserFieldsFormDef(FormDef):
     '''Class to handle custom user fields, it loads and saves from/to
@@ -405,6 +429,7 @@ class SettingsDirectory(QommonSettingsDirectory):
             'session', 'download_theme', 'smstest', 'postgresql',
             ('admin-permissions', 'admin_permissions'), 'geolocation',
             'theme_preview', 'filetypes',
+            ('user-template', 'user_template'),
             ('data-sources', 'data_sources'), 'wscalls', 'logs']
 
     emails = EmailsDirectory()
@@ -517,6 +542,10 @@ class SettingsDirectory(QommonSettingsDirectory):
         if enabled('users'):
             r += htmltext('<dt><a href="users/">%s</a></dt> <dd>%s</dd>') % (
                     _('Users'), _('Configure users'))
+        else:
+            # minimal options
+            r += htmltext('<dt><a href="user-template">%s</a></dt> <dd>%s</dd>') % (
+                    _('Users'), _('Configure sidebar template for users'))
         if enabled('emails'):
             r += htmltext('<dt><a href="emails/">%s</a></dt> <dd>%s</dd>') % (
                     _('Emails'), _('Configure email settings'))
@@ -1223,3 +1252,23 @@ class SettingsDirectory(QommonSettingsDirectory):
         r += htmltext('<h2>%s</h2>') % _('Geolocation Settings')
         r += form.render()
         return r.getvalue()
+
+    def user_template(self):
+        users_cfg = get_cfg('users', {})
+        form = UserFieldsDirectory.sidebar_template_form(action='user-template')
+        form.get_widget('sidebar_template').set_title(_('Sidebar Template'))
+        form.add_submit('cancel', _('Cancel'))
+
+        if form.get_widget('cancel').parse():
+            return redirect('.')
+
+        if form.is_submitted() and not form.has_errors():
+            cfg_submit(form, 'users', ['sidebar_template'])
+            return redirect('.')
+
+        get_response().breadcrumb.append(('user-template', _('Users')))
+        html_top('settings', title=_('Users'))
+        r = TemplateIO(html=True)
+        r += htmltext('<h2>%s</h2>') % _('Users')
+        r += form.render()
+        return r.getvalue()
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 10a84785b..ac70afb29 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -53,6 +53,7 @@ from ..qommon import ods
 from ..qommon.form import *
 from ..qommon.storage import (Equal, NotEqual, LessOrEqual, GreaterOrEqual, Or,
         Intersects, ILike, FtsMatch, Contains, Null, NotNull)
+from ..qommon.template import Template
 
 from wcs.api_utils import get_user_from_api_query_string
 from wcs.carddef import CardDef
@@ -2661,7 +2662,16 @@ class FormBackOfficeStatusPage(FormStatusPage):
         if formdata.user_id and formdata.get_user():
             r += htmltext('<div class="extra-context">')
             r += htmltext('<h3>%s</h3>') % _('Associated User')
-            r += htmltext('<p>%s</p>') % formdata.get_user().display_name
+            users_cfg = get_cfg('users', {})
+            sidebar_user_template = users_cfg.get('sidebar_template')
+            if sidebar_user_template:
+                variables = get_publisher().substitutions.get_context_variables(mode='lazy')
+                sidebar_user = Template(sidebar_user_template).render(variables)
+                if not sidebar_user.startswith('<'):
+                    sidebar_user = htmltext('<p>%s</p>' % sidebar_user)
+                r += sidebar_user
+            else:
+                r += htmltext('<p>%s</p>') % formdata.get_user().display_name
             r += htmltext('</div>')
 
         if formdata.formdef.geolocations and formdata.geolocations:
-- 
2.27.0.rc0

