From d774f8a7dd8faed6738a413172333fe77fec3004 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 31 Jul 2020 13:56:08 +0200
Subject: [PATCH] backoffice: allow adding custom user column to management
 tables (#40031)

---
 tests/test_backoffice_pages.py | 40 ++++++++++++++++++++++++++++++++++
 wcs/backoffice/management.py   | 37 ++++++++++++++++++++++++++-----
 wcs/sql.py                     | 26 +++++++++++++---------
 wcs/users.py                   |  5 +++++
 4 files changed, 93 insertions(+), 15 deletions(-)

diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index 930c48729..aa20cb021 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -647,6 +647,46 @@ def test_backoffice_image_column(pub):
     assert 'download?f=4&thumbnail=1' not in resp.text
 
 
+def test_backoffice_user_columns(pub):
+    create_superuser(pub)
+    create_environment(pub)
+
+    from wcs.admin.settings import UserFieldsFormDef
+    user_formdef = UserFieldsFormDef(pub)
+    user_formdef.fields.append(fields.StringField(id='_first_name', label='name', type='string'))
+    user_formdef.fields.append(fields.StringField(id='3', label='test', type='string'))
+    user_formdef.store()
+    pub.cfg['users']['field_name'] = ['3', '4']
+    pub.write_cfg()
+
+    user1 = pub.user_class(name='userA')
+    user1.form_data = {'_first_name': 'toto', '3': 'nono'}
+    user1.set_attributes_from_formdata(user1.form_data)
+    user1.store()
+    user2 = pub.user_class(name='userB')
+    user2.form_data = {'_first_name': 'tutu', '3': 'nunu'}
+    user2.set_attributes_from_formdata(user2.form_data)
+    user2.store()
+
+    formdef = FormDef.get_by_urlname('form-title')
+    for i, formdata in enumerate(formdef.data_class().select()):
+        formdata.user_id = user1.id if bool(i % 2) else user2.id
+        formdata.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/management/form-title/')
+    assert resp.text.count('</th>') == 8  # six columns
+
+    if not pub.is_using_postgresql():
+        # no support for relation columns unless using SQL
+        assert 'user-label$3' not in resp.forms['listing-settings'].fields
+        return
+    resp.forms['listing-settings']['user-label$3'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+    assert resp.text.count('</th>') == 9
+    assert '<td>nono</td' in resp
+
+
 def test_backoffice_card_field_columns(pub):
     user = create_superuser(pub)
     create_environment(pub)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 9ca3a5673..17a107b6e 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1427,9 +1427,9 @@ class FormPage(Directory):
                 attrs = ''
                 if isinstance(field, RelatedField):
                     classnames = 'related-field'
-                    if field.parent_field.id in seen_parents:
+                    if field.parent_field_id in seen_parents:
                         classnames += ' collapsed'
-                    attrs = 'data-relation-attr="%s"' % field.parent_field.id
+                    attrs = 'data-relation-attr="%s"' % field.parent_field_id
                 elif getattr(field, 'has_relations', False):
                     classnames = 'has-relations-field'
                     attrs = 'data-field-id="%s"' % field.id
@@ -1537,7 +1537,16 @@ class FormPage(Directory):
             yield FakeField('submission_agent', 'submission_agent', _('Submission By'))
         yield FakeField('time', 'time', _('Created'))
         yield FakeField('last_update_time', 'last_update_time', _('Last Modified'))
+
+        # user fields
         yield FakeField('user-label', 'user-label', _('User Label'))
+        if get_publisher().is_using_postgresql():
+            for field in get_publisher().user_class.get_fields():
+                if not hasattr(field, 'get_view_value'):
+                    continue
+                field.has_relations = True
+                yield UserRelatedField(field)
+
         for field in self.formdef.get_all_fields():
             yield field
             if not get_publisher().is_using_postgresql():
@@ -3160,6 +3169,10 @@ class FakeField(object):
     def get_csv_value(self, element, **kwargs):
         return [element]
 
+    @property
+    def has_relations(self):
+        return bool(self.id == 'user-label')
+
 
 class RelatedField:
     is_related_field = True
@@ -3169,16 +3182,17 @@ class RelatedField:
 
     def __init__(self, carddef, field, parent_field):
         self.carddef = carddef
-        self.carddef_field = field
+        self.related_field = field
         self.parent_field = parent_field
+        self.parent_field_id = parent_field.id
 
     @property
     def id(self):
-        return '%s$%s' % (self.parent_field.id, self.carddef_field.id)
+        return '%s$%s' % (self.parent_field_id, self.related_field.id)
 
     @property
     def label(self):
-        return '%s - %s' % (self.parent_field.label, self.carddef_field.label)
+        return '%s - %s' % (self.parent_field.label, self.related_field.label)
 
     def get_view_value(self, value, **kwargs):
         if value is None:
@@ -3199,6 +3213,19 @@ class RelatedField:
         return [self.get_view_value(value)]
 
 
+class UserRelatedField(RelatedField):
+    # it is named 'user-label' and not 'user' for compatibility with existing
+    # listings, as the 'classic' user column is named 'user-label'.
+    parent_field_id = 'user-label'
+
+    def __init__(self, field):
+        self.related_field = field
+
+    @property
+    def label(self):
+        return _('%s of User') % self.related_field.label
+
+
 def do_graphs_section(period_start=None, period_end=None, criterias=None):
     from wcs import sql
     r = TemplateIO(html=True)
diff --git a/wcs/sql.py b/wcs/sql.py
index f396638fb..65b6d81ae 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -1156,19 +1156,25 @@ class SqlMixin(object):
             for field in fields:
                 if not getattr(field, 'is_related_field', False):
                     continue
-                carddef_dataclass = field.carddef.data_class()
-                carddef_table_alias = 't%s' % id(field.carddef)
-                carddef_table_decl = 'LEFT JOIN %s AS %s ON (CAST(%s.%s AS INTEGER) = %s.id)' % (
-                        carddef_dataclass._table_name,
-                        carddef_table_alias,
-                        cls._table_name,
-                        get_field_id(field.parent_field),
-                        carddef_table_alias)
+                if field.parent_field_id == 'user-label':
+                    # relation to user table
+                    carddef_table_alias = 'users'
+                    carddef_table_decl = 'LEFT JOIN users ON (CAST(%s.user_id AS INTEGER) = users.id)' % cls._table_name
+                else:
+                    carddef_dataclass = field.carddef.data_class()
+                    carddef_table_alias = 't%s' % id(field.carddef)
+                    carddef_table_decl = 'LEFT JOIN %s AS %s ON (CAST(%s.%s AS INTEGER) = %s.id)' % (
+                            carddef_dataclass._table_name,
+                            carddef_table_alias,
+                            cls._table_name,
+                            get_field_id(field.parent_field),
+                            carddef_table_alias)
+
                 if carddef_table_decl not in tables:
                     tables.append(carddef_table_decl)
 
-                column_field_id = get_field_id(field.carddef_field)
-                if field.carddef_field.store_display_value:
+                column_field_id = get_field_id(field.related_field)
+                if field.related_field.store_display_value:
                     column_field_id += '_display'
                 columns.append('%s.%s' % (carddef_table_alias, column_field_id))
                 extra_fields.append(field.id)
diff --git a/wcs/users.py b/wcs/users.py
index eb241391a..760516ef7 100644
--- a/wcs/users.py
+++ b/wcs/users.py
@@ -80,6 +80,11 @@ class User(StorableObject):
         from .admin.settings import UserFieldsFormDef
         return UserFieldsFormDef()
 
+    @classmethod
+    def get_fields(cls):
+        formdef = cls.get_formdef()
+        return formdef.fields or []
+
     @property
     def ascii_name(self):
         return simplify(self.get_display_name(), space=' ')
-- 
2.28.0

