From 65ddc83b9422d6f195c431e77eab5008d3166174 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sat, 8 Jun 2019 14:36:35 +0200
Subject: [PATCH] backoffice: create/display user label from data fields
 (#33812)

---
 tests/test_backoffice_pages.py | 38 +++++++++++++++++++++++
 tests/test_formdata.py         | 57 ++++++++++++++++++++++++++++++++++
 wcs/backoffice/management.py   |  6 ++--
 wcs/formdata.py                | 30 +++++++++++++++---
 wcs/sql.py                     | 20 ++++++++----
 5 files changed, 138 insertions(+), 13 deletions(-)

diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index 361be7b36..1893ea30f 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -2826,6 +2826,44 @@ def test_global_listing(pub):
     assert resp.body[resp.body.index('<tbody'):].count('<tr') == 0
     assert not 'form-title' in resp.body
 
+def test_global_listing_user_label(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
+    create_user(pub)
+    FormDef.wipe()
+
+    from wcs.admin.settings import UserFieldsFormDef
+    user_formdef = UserFieldsFormDef(pub)
+    user_formdef.fields.append(fields.StringField(id='3', label='first_name', type='string'))
+    user_formdef.fields.append(fields.StringField(id='4', label='last_name', type='string'))
+    user_formdef.store()
+    pub.cfg['users']['field_name'] = ['3', '4']
+    pub.write_cfg()
+
+    formdef = FormDef()
+    formdef.name = 'foobar'
+    formdef.url_name = 'foobar'
+    formdef.workflow_roles = {'_receiver': 1}
+    formdef.fields = [
+        fields.StringField(id='1', label='first_name',
+        prefill={'type': 'user', 'value': '3'}),
+        fields.StringField(id='2', label='last_name',
+        prefill={'type': 'user', 'value': '4'}),
+    ]
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {'1': 'blah', '2': 'xxx'}
+    formdata.just_created()
+    formdata.store()
+    formdata.jump_status('new')
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/').follow()
+    resp = resp.click('Global View')
+    assert '<td class="cell-user">blah xxx</td>' in resp.body
 
 def test_management_views_with_no_formdefs(pub):
     if not pub.is_using_postgresql():
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 64302f457..0eb306685 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -1618,3 +1618,60 @@ def test_string_filters(pub, variable_test_data):
     for mode in (None, 'lazy'):
         context = pub.substitutions.get_context_variables(mode=mode)
         assert tmpl.render(context) == ''
+
+def test_user_label(pub):
+    from wcs.admin.settings import UserFieldsFormDef
+    user_formdef = UserFieldsFormDef(pub)
+    user_formdef.fields.append(fields.StringField(id='3', label='first_name', type='string'))
+    user_formdef.fields.append(fields.StringField(id='4', label='last_name', type='string'))
+    user_formdef.store()
+    pub.cfg['users']['field_name'] = ['3', '4']
+    pub.write_cfg()
+
+    formdef = FormDef()
+    formdef.name = 'foobar'
+    formdef.url_name = 'foobar'
+    formdef.fields = [
+        fields.StringField(id='1', label='first_name',
+        prefill={'type': 'user', 'value': '3'}),
+        fields.StringField(id='2', label='last_name',
+        prefill={'type': 'user', 'value': '4'}),
+    ]
+    formdef.store()
+
+    user = pub.user_class()
+    user.email = 'bar@localhost'
+    user.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.store()
+
+    assert str(formdef.data_class().get(formdata.id).user_id) == str(user.id)
+    assert formdef.data_class().get(formdata.id).user_label is None
+    assert formdef.data_class().get(formdata.id).get_user_label() == 'bar@localhost'
+
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.store()
+
+    assert formdef.data_class().get(formdata.id).user_id is None
+    assert formdef.data_class().get(formdata.id).user_label == ''
+    assert formdef.data_class().get(formdata.id).get_user_label() == ''
+
+    formdata = formdef.data_class()()
+    formdata.data = {'1': 'blah'}
+    formdata.store()
+
+    assert formdef.data_class().get(formdata.id).user_id is None
+    assert formdef.data_class().get(formdata.id).user_label == 'blah'
+    assert formdef.data_class().get(formdata.id).get_user_label() == 'blah'
+
+    formdata = formdef.data_class()()
+    formdata.data = {'1': 'blah', '2': 'xxx'}
+    formdata.store()
+
+    assert formdef.data_class().get(formdata.id).user_id is None
+    assert formdef.data_class().get(formdata.id).user_label == 'blah xxx'
+    assert formdef.data_class().get(formdata.id).get_user_label() == 'blah xxx'
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index dbe7c2ba2..13005b904 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -903,10 +903,10 @@ class ManagementDirectory(Directory):
                     formdata.receipt_time)
             r += htmltext('<td class="cell-time">%s</td>') % misc.localstrftime(
                     formdata.last_update_time)
-            try:
-                value = get_publisher().user_class.get(formdata.user_id).display_name
+            value = formdata.get_user_label()
+            if value:
                 r += htmltext('<td class="cell-user">%s</td>') % value
-            except:
+            else:
                 r += htmltext('<td class="cell-user cell-no-user">-</td>')
             r += htmltext('<td class="cell-status">%s</td>') % formdata.get_status_label()
             r += htmltext('</tr>\n')
diff --git a/wcs/formdata.py b/wcs/formdata.py
index a303955b0..2fb75ef7c 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -29,6 +29,7 @@ from qommon import _
 from qommon.storage import StorableObject, Intersects, Contains
 import qommon.misc
 from qommon.evalutils import make_datetime
+from qommon.publisher import get_cfg
 from qommon.substitution import Substitutions, invalidate_substitution_cache
 from qommon.template import Template
 
@@ -245,6 +246,7 @@ class FormData(StorableObject):
     id_display = None
 
     user_id = None
+    user_label = None  # taken from data, for anonymous users
     receipt_time = None
     status = None
     anonymised = None
@@ -323,6 +325,12 @@ class FormData(StorableObject):
             self.user_id = None
     user = property(get_user, set_user)
 
+    def get_user_label(self):
+        user = self.user
+        if user:
+            return user.get_display_name()
+        return self.user_label
+
     def has_empty_data(self):
         empty = True
         for key in self.data or {}:
@@ -386,7 +394,23 @@ class FormData(StorableObject):
             # only set id_display once as it may have been set automatically
             # by interpreting a webservice response.
             fields['id_display'] = self.formdef.get_display_id_format().strip()
+
         changed = False
+
+        users_cfg = get_cfg('users', {})
+        if not self.user_id and users_cfg and users_cfg.get('field_name'):
+            field_name_values = users_cfg.get('field_name')
+            form_user_data = {}
+            for field in self.formdef.fields:
+                if not hasattr(field, 'prefill'):
+                    continue
+                if field.prefill and field.prefill.get('type') == 'user':
+                    form_user_data[field.prefill['value']] = self.data.get(field.id)
+            user_label = ' '.join([form_user_data.get(x) for x in field_name_values if form_user_data.get(x)])
+            if user_label != self.user_label:
+                self.user_label = user_label
+                changed = True
+
         if any(fields.values()):
             context = self.get_substitution_variables()
             context['formdef_id'] = self.formdef.id
@@ -610,10 +634,7 @@ class FormData(StorableObject):
         if field.type == 'last_update_time':
             return qommon.misc.localstrftime(self.last_update_time)
         if field.type == 'user-label':
-            try:
-                return get_publisher().user_class.get(self.user_id).display_name
-            except KeyError:
-                return '-'
+            return self.get_user_label() or '-'
         if field.type == 'status':
             return self.get_status_label()
         if field.type == 'submission_channel':
@@ -932,6 +953,7 @@ class FormData(StorableObject):
 
         self.anonymised = datetime.datetime.now()
         self.user_id = None
+        self.user_label = None
         self.editable_by = None
         self.workflow_data = None
         self.workflow_roles = None
diff --git a/wcs/sql.py b/wcs/sql.py
index c4a1c0de3..920b8bf70 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -403,7 +403,7 @@ def do_formdef_tables(formdef, conn=None, cur=None, rebuild_views=False, rebuild
         'actions_roles_array', 'backoffice_submission',
         'submission_context', 'submission_channel',
         'criticality_level', 'last_update_time',
-        'digest'])
+        'digest', 'user_label'])
 
     # migrations
     if not 'fts' in existing_fields:
@@ -447,6 +447,9 @@ def do_formdef_tables(formdef, conn=None, cur=None, rebuild_views=False, rebuild
     if not 'digest' in existing_fields:
         cur.execute('''ALTER TABLE %s ADD COLUMN digest varchar''' % table_name)
 
+    if not 'user_label' in existing_fields:
+        cur.execute('''ALTER TABLE %s ADD COLUMN user_label varchar''' % table_name)
+
     # add new fields
     for field in formdef.get_all_fields():
         assert field.id is not None
@@ -733,7 +736,7 @@ def get_view_fields(formdef):
     view_fields.append(("int '%s'" % (formdef.id or 0), 'formdef_id'))
     for field in ('id', 'user_id', 'receipt_time', 'status',
             'id_display', 'submission_channel', 'backoffice_submission',
-            'last_update_time', 'digest'):
+            'last_update_time', 'digest', 'user_label'):
         view_fields.append((field, field))
     return view_fields
 
@@ -1207,6 +1210,7 @@ class SqlFormData(SqlMixin, wcs.formdata.FormData):
         ('criticality_level', 'int'),
         ('last_update_time', 'timestamp'),
         ('digest', 'varchar'),
+        ('user_label', 'varchar'),
     ]
 
     def __init__(self, id=None):
@@ -1363,12 +1367,14 @@ class SqlFormData(SqlMixin, wcs.formdata.FormData):
         if self.set_auto_fields():
             sql_statement = '''UPDATE %s
                                   SET id_display = %%(id_display)s,
-                                      digest = %%(digest)s
+                                      digest = %%(digest)s,
+                                      user_label = %%(user_label)s
                                 WHERE id = %%(id)s''' % self._table_name
             cur.execute(sql_statement, {
                 'id': self.id,
                 'id_display': self.id_display,
                 'digest': self.digest,
+                'user_label': self.user_label,
             })
 
         if self._evolution:
@@ -2168,7 +2174,7 @@ def get_yearly_totals(period_start=None, period_end=None, criterias=None):
     return result
 
 
-SQL_LEVEL = 30
+SQL_LEVEL = 31
 
 def migrate_global_views(conn, cur):
     cur.execute('''SELECT COUNT(*) FROM information_schema.tables
@@ -2235,7 +2241,7 @@ def migrate():
         raise RuntimeError()
     if sql_level < 1: # 1: introduction of tracking_code table
         do_tracking_code_table()
-    if sql_level < 27:
+    if sql_level < 31:
         # 2: introduction of formdef_id in views
         # 5: add concerned_roles_array, is_at_endpoint and fts to views
         # 7: add backoffice_submission to tables
@@ -2251,6 +2257,7 @@ def migrate():
         # 22: rebuild views
         # 26: add digest to formdata
         # 27: add last_jump_datetime in evolutions tables
+        # 31: add user_label to formdata
         migrate_views(conn, cur)
     if sql_level < 21:
         # 3: introduction of _structured for user fields
@@ -2270,13 +2277,14 @@ def migrate():
         # 21: (second part), store ascii_name of users
         # 23: (first part), use misc.simplify() over full text queries
         set_reindex('user', 'needed', conn=conn, cur=cur)
-    if sql_level < 28:
+    if sql_level < 31:
         # 17: store last_update_time in tables
         # 18: add user name to full-text search index
         # 21: (third part), add user ascii_names to full-text index
         # 23: (second part) use misc.simplify() over full text queries
         # 28: add display id and formdef name to full-text index
         # 29: add evolution parts to full-text index
+        # 31: add user_label to formdata
         set_reindex('formdata', 'needed', conn=conn, cur=cur)
     if sql_level < 24:
         from wcs.formdef import FormDef
-- 
2.20.1

