From 8727c72d36fbb713722c86ef4a324de8eab2d9b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 17 Jan 2022 12:05:52 +0100
Subject: [PATCH 3/4] backoffice: formdata display fields in block field
 (#46500)

---
 tests/backoffice_pages/test_columns.py | 48 +++++++++++++++-
 tests/backoffice_pages/test_filters.py | 10 ++--
 wcs/backoffice/management.py           | 60 +++++++++-----------
 wcs/formdata.py                        | 78 ++++++++++++++------------
 wcs/forms/backoffice.py                |  7 ++-
 5 files changed, 123 insertions(+), 80 deletions(-)

diff --git a/tests/backoffice_pages/test_columns.py b/tests/backoffice_pages/test_columns.py
index 76451910..f7905ee2 100644
--- a/tests/backoffice_pages/test_columns.py
+++ b/tests/backoffice_pages/test_columns.py
@@ -423,17 +423,39 @@ def test_backoffice_card_field_columns(pub):
 
 
 def test_backoffice_block_columns(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
     pub.user_class.wipe()
     create_superuser(pub)
     pub.role_class.wipe()
     role = pub.role_class(name='test')
     role.store()
 
+    CardDef.wipe()
+    carddef = CardDef()
+    carddef.name = 'foo'
+    carddef.fields = [
+        fields.StringField(id='1', label='First Name', type='string', varname='first_name'),
+        fields.StringField(id='2', label='Last Name', type='string', varname='last_name'),
+    ]
+    carddef.digest_templates = {'default': '{{ form_var_first_name }} {{ form_var_last_name }}'}
+    carddef.store()
+    carddef.data_class().wipe()
+    card = carddef.data_class()()
+    card.data = {
+        '1': 'Foo',
+        '2': 'Bar',
+    }
+    card.store()
+
     BlockDef.wipe()
     block = BlockDef()
     block.name = 'foobar'
     block.fields = [
         fields.StringField(id='123', required=True, label='Test', type='string'),
+        fields.ItemField(id='456', label='card field', type='item', data_source={'type': 'carddef:foo'}),
     ]
     block.store()
 
@@ -446,9 +468,23 @@ def test_backoffice_block_columns(pub):
     formdef.workflow_roles = {'_receiver': role.id}
     formdef.store()
 
+    data_class = formdef.data_class()
+    data_class.wipe()
+
+    formdata = data_class()
+    formdata.data = {
+        '8': {
+            'data': [{'123': 'blah', '456': card.id, '456_display': card.default_digest}],
+            'schema': {},  # not important here
+        },
+        '8_display': 'blah',
+    }
+    formdata.just_created()
+    formdata.jump_status('new')
+    formdata.store()
+
     app = login(get_app(pub))
     resp = app.get('/backoffice/management/form-title/')
-    # check Block / Test is not part of the possible columns
     assert [x.text_content() for x in resp.pyquery('#columns-filter label')] == [
         'Number',
         'Created',
@@ -456,5 +492,15 @@ def test_backoffice_block_columns(pub):
         'User Label',
         'Status',
         'Block',
+        'Block / Test',
+        'Block / card field',
         'Anonymised',
     ]
+    resp.forms['listing-settings']['8-123'].checked = True
+    resp.forms['listing-settings']['8-456'].checked = True
+    resp = resp.forms['listing-settings'].submit()
+    assert '<th><span>Block / Test</span></th>' in resp
+    assert '<th><span>Block / card field</span></th>' in resp
+    assert resp.text.count('<tr') == 2
+    assert '<td>blah</td>' in resp
+    assert '<td>Foo Bar</td>' in resp
diff --git a/tests/backoffice_pages/test_filters.py b/tests/backoffice_pages/test_filters.py
index e987fab7..3675bbda 100644
--- a/tests/backoffice_pages/test_filters.py
+++ b/tests/backoffice_pages/test_filters.py
@@ -980,11 +980,11 @@ def test_backoffice_block_field_filter(pub):
 
     app = login(get_app(pub))
     resp = app.get('/backoffice/management/form-title/')
-    assert '<label><input type="checkbox" name="1"/>Block Data / String</label>' not in resp
-    assert '<label><input type="checkbox" name="2"/>Block Data / Item</label>' not in resp
-    assert '<label><input type="checkbox" name="3"/>Block Data / Bool</label>' not in resp
-    assert '<label><input type="checkbox" name="4"/>Block Data / Date</label>' not in resp
-    assert '<label><input type="checkbox" name="5"/>Block Data / Email</label>' not in resp
+    assert '<label><input type="checkbox" name="0-1"/>Block Data / String</label>' in resp
+    assert '<label><input type="checkbox" name="0-2"/>Block Data / Item</label>' in resp
+    assert '<label><input type="checkbox" name="0-3"/>Block Data / Bool</label>' in resp
+    assert '<label><input type="checkbox" name="0-4"/>Block Data / Date</label>' in resp
+    assert '<label><input type="checkbox" name="0-5"/>Block Data / Email</label>' in resp
 
     # string
     resp = app.get('/backoffice/management/form-title/')
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 34794603..84eb1a91 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -993,10 +993,6 @@ class FormPage(Directory):
         get_request().is_json_marker = True
         field_id = get_request().form.get('filter_field_id')
         for filter_field in self.get_formdef_fields():
-            is_in_block_field = getattr(filter_field, 'block_field', None)
-            filter_field.contextual_id = filter_field.id
-            if is_in_block_field:
-                filter_field.contextual_id = '%s-%s' % (filter_field.block_field.id, filter_field.id)
             if filter_field.contextual_id == field_id:
                 break
         else:
@@ -1054,19 +1050,8 @@ class FormPage(Directory):
                 continue
             filter_fields.append(field)
 
-            # add contextual_id/contextual_varname attributes
-            # they are id/varname for normal fields
-            # but in case of blocks they are concatenation of block id/varname + field id/varname
-            is_in_block_field = getattr(field, 'block_field', None)
-            field.contextual_id = field.id
-            field.contextual_varname = None
-            if is_in_block_field:
-                field.contextual_id = '%s-%s' % (field.block_field.id, field.id)
+            if getattr(field, 'block_field', None):
                 field.label = '%s / %s' % (field.block_field.label, field.label)
-                if field.varname and field.block_field.varname:
-                    field.contextual_varname = '%s_%s' % (field.block_field.varname, field.varname)
-            else:
-                field.contextual_varname = field.varname
 
             if get_request().form:
                 field.enabled = ('filter-%s' % field.contextual_id in get_request().form) or (
@@ -1351,9 +1336,6 @@ class FormPage(Directory):
             for field in sorted(self.get_formdef_fields(), key=get_column_position):
                 if not hasattr(field, 'get_view_value'):
                     continue
-                if getattr(field, 'block_field', None):
-                    # fields from blocks cannot yet be added as column
-                    continue
                 classnames = ''
                 attrs = ''
                 if isinstance(field, RelatedField):
@@ -1366,7 +1348,7 @@ class FormPage(Directory):
                     attrs = 'data-field-id="%s"' % field.id
                     seen_parents.add(field.id)
                 r += htmltext('<li class="%s" %s><span class="handle">⣿</span>' % (classnames, attrs))
-                r += htmltext('<label><input type="checkbox" name="%s"') % field.id
+                r += htmltext('<label><input type="checkbox" name="%s"') % field.contextual_id
                 if field.id in field_ids:
                     r += htmltext(' checked="checked"')
                 r += htmltext('/>')
@@ -1565,17 +1547,32 @@ class FormPage(Directory):
 
         def iter_fields(fields, block_field=None):
             for field in fields:
+                # add contextual_id/contextual_varname attributes
+                # they are id/varname for normal fields
+                # but in case of blocks they are concatenation of block id/varname + field id/varname
+                field.contextual_id = field.id
+                field.contextual_varname = None
                 if block_field:
                     if field.key == 'items':
                         # not yet
                         continue
                     field.block_field = block_field
+                    field.contextual_id = '%s-%s' % (field.block_field.id, field.id)
+                    if field.varname and field.block_field.varname:
+                        field.contextual_varname = '%s_%s' % (
+                            field.block_field.varname,
+                            field.varname,
+                        )
+                else:
+                    field.contextual_varname = field.varname
                 yield field
                 if not get_publisher().is_using_postgresql():
                     continue
                 if field.key == 'block':
                     yield from iter_fields(field.block.fields, block_field=field)
                     continue
+                if block_field:
+                    continue
                 if not (
                     field.type == 'item'
                     and field.data_source
@@ -1615,7 +1612,7 @@ class FormPage(Directory):
 
         fields = []
         for field in self.get_formdef_fields():
-            if field.id in field_ids:
+            if field.contextual_id in field_ids:
                 fields.append(field)
 
         if 'columns-order' in get_request().form or self.view:
@@ -1681,19 +1678,6 @@ class FormPage(Directory):
 
             filter_field_key = None
 
-            is_in_block_field = getattr(filter_field, 'block_field', None)
-            filter_field.contextual_id = filter_field.id
-            filter_field.contextual_varname = None
-            if is_in_block_field:
-                filter_field.contextual_id = '%s-%s' % (filter_field.block_field.id, filter_field.id)
-                if filter_field.varname and filter_field.block_field.varname:
-                    filter_field.contextual_varname = '%s_%s' % (
-                        filter_field.block_field.varname,
-                        filter_field.varname,
-                    )
-            else:
-                filter_field.contextual_varname = filter_field.varname
-
             if filter_field.contextual_varname:
                 # if this is a field with a varname and filter-%(varname)s is
                 # present in the query string, enable this filter.
@@ -1771,7 +1755,7 @@ class FormPage(Directory):
                 else:
                     raise RequestError('Invalid value "%s" for "%s"' % (filter_field_value, filter_field_key))
 
-            if is_in_block_field:
+            if getattr(filter_field, 'block_field', None):
                 criterias.append(
                     sql.ArrayContains(
                         'f%s' % filter_field.block_field.id,
@@ -3433,10 +3417,12 @@ class FormBackOfficeStatusPage(FormStatusPage):
 class FakeField:
     def __init__(self, id, type_, label, addable=True):
         self.id = id
+        self.contextual_id = self.id
         self.type = type_
         self.label = force_text(label)
         self.fake = True
         self.varname = id.replace('-', '_')
+        self.contextual_varname = self.varname
         self.store_display_value = None
         self.addable = addable
 
@@ -3471,6 +3457,10 @@ class RelatedField:
     def id(self):
         return '%s$%s' % (self.parent_field_id, self.related_field.id)
 
+    @property
+    def contextual_id(self):
+        return self.id
+
     @property
     def label(self):
         return '%s - %s' % (self.parent_field.label, self.related_field.label)
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 67bb9e0c..a33a61a4 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -797,42 +797,48 @@ class FormData(StorableObject):
             return None
 
     def get_field_view_value(self, field, max_length=None):
-        # return the value of the given field, with special handling for "fake"
-        # field types that are shortcuts to internal properties.
-        if field.type == 'id':
-            return self.get_display_id()
-        if field.type == 'display_name':
-            return self.get_display_name()
-        if field.type == 'time':
-            return misc.localstrftime(self.receipt_time)
-        if field.type == 'last_update_time':
-            return misc.localstrftime(self.last_update_time)
-        if field.type == 'user-label':
-            return self.get_user_label() or '-'
-        if field.type == 'status':
-            return self.get_status_label()
-        if field.type == 'submission_channel':
-            return self.get_submission_channel_label()
-        if field.type == 'submission_agent':
-            try:
-                agent_user = self.submission_agent_id
-                return get_publisher().user_class.get(agent_user).display_name
-            except (KeyError, TypeError):
-                return '-'
-        if field.type == 'anonymised':
-            return _('Yes') if self.anonymised else _('No')
-
-        field_value = self.data.get(field.id)
-        if field_value is None:
-            return ''
-        if max_length is not None:
-            # if max_length is set the target is a backoffice listing/table,
-            # return an html value, appropriately shortened.
-            field_value = self.data.get('%s_display' % field.id, field_value)
-            return field.get_view_short_value(field_value, max_length)
-        else:
-            # otherwise return the actual "raw" field value
-            return field_value
+        def get_value(field, data):
+            # return the value of the given field, with special handling for "fake"
+            # field types that are shortcuts to internal properties.
+            if field.type == 'id':
+                return self.get_display_id()
+            if field.type == 'display_name':
+                return self.get_display_name()
+            if field.type == 'time':
+                return misc.localstrftime(self.receipt_time)
+            if field.type == 'last_update_time':
+                return misc.localstrftime(self.last_update_time)
+            if field.type == 'user-label':
+                return self.get_user_label() or '-'
+            if field.type == 'status':
+                return self.get_status_label()
+            if field.type == 'submission_channel':
+                return self.get_submission_channel_label()
+            if field.type == 'submission_agent':
+                try:
+                    agent_user = self.submission_agent_id
+                    return get_publisher().user_class.get(agent_user).display_name
+                except (KeyError, TypeError):
+                    return '-'
+            if field.type == 'anonymised':
+                return _('Yes') if self.anonymised else _('No')
+
+            field_value = data.get(field.id)
+            if field_value is None:
+                return ''
+            if max_length is not None:
+                # if max_length is set the target is a backoffice listing/table,
+                # return an html value, appropriately shortened.
+                field_value = data.get('%s_display' % field.id, field_value)
+                return field.get_view_short_value(field_value, max_length)
+            else:
+                # otherwise return the actual "raw" field value
+                return field_value
+
+        if getattr(field, 'block_field', None):
+            data = self.data.get(field.block_field.id) or {}
+            return ', '.join(get_value(field, d) for d in data.get('data') or [])
+        return get_value(field, self.data)
 
     def update_workflow_data(self, dict):
         if not self.workflow_data:
diff --git a/wcs/forms/backoffice.py b/wcs/forms/backoffice.py
index 214ff9cd..19ff2eb1 100644
--- a/wcs/forms/backoffice.py
+++ b/wcs/forms/backoffice.py
@@ -105,22 +105,23 @@ class FormDefUI:
                 ' <span id="info-all-rows"><label><input type="checkbox" name="select[]" value="_all"/> %s</label></span></th>'
             ) % _('Run selected action on all pages')
         for f in fields:
-            field_sort_key = None
             if getattr(f, 'fake', False):
                 field_sort_key = f.id
                 if f.id == 'time':
                     field_sort_key = 'receipt_time'
                 elif f.id in ('user-label', 'submission_agent'):
                     field_sort_key = None
-            elif getattr(f, 'is_related_field', False):
+            elif getattr(f, 'is_related_field', False) or getattr(f, 'block_field', None):
                 field_sort_key = None
             else:
-                field_sort_key = 'f%s' % f.id
+                field_sort_key = 'f%s' % f.contextual_id
 
             if field_sort_key and using_postgresql:
                 r += htmltext('<th data-field-sort-key="%s">') % field_sort_key
             else:
                 r += htmltext('<th>')
+            if getattr(f, 'block_field', None):
+                f.label = '%s / %s' % (f.block_field.label, f.label)
             if len(f.label) < 20:
                 r += htmltext('<span>%s</span>') % f.label
             else:
-- 
2.34.1

