From ff4b023c8e59e2ec39c4302e401c588d95809c13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 18 Jan 2022 09:45:17 +0100
Subject: [PATCH 4/4] backoffice: sorting on a field of block field (#46500)

---
 tests/backoffice_pages/test_columns.py | 72 ++++++++++++++++++++++++--
 wcs/forms/backoffice.py                | 31 ++++++++++-
 wcs/sql.py                             | 10 +++-
 3 files changed, 105 insertions(+), 8 deletions(-)

diff --git a/tests/backoffice_pages/test_columns.py b/tests/backoffice_pages/test_columns.py
index f7905ee2..c15adea5 100644
--- a/tests/backoffice_pages/test_columns.py
+++ b/tests/backoffice_pages/test_columns.py
@@ -471,17 +471,17 @@ def test_backoffice_block_columns(pub):
     data_class = formdef.data_class()
     data_class.wipe()
 
-    formdata = data_class()
-    formdata.data = {
+    formdata1 = data_class()
+    formdata1.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()
+    formdata1.just_created()
+    formdata1.jump_status('new')
+    formdata1.store()
 
     app = login(get_app(pub))
     resp = app.get('/backoffice/management/form-title/')
@@ -504,3 +504,65 @@ def test_backoffice_block_columns(pub):
     assert resp.text.count('<tr') == 2
     assert '<td>blah</td>' in resp
     assert '<td>Foo Bar</td>' in resp
+
+    formdef.fields[0].max_items = 1
+    formdef.store()
+
+    card2 = carddef.data_class()()
+    card2.data = {
+        '1': 'Bar2',
+        '2': 'Foo2',
+    }
+    card2.store()
+
+    formdata2 = data_class()
+    formdata2.data = {
+        '8': {
+            'data': [{'123': 'blah', '456': card2.id, '456_display': card2.default_digest}],
+            'schema': {},  # not important here
+        },
+        '8_display': 'blah',
+    }
+    formdata2.just_created()
+    formdata2.jump_status('new')
+    formdata2.store()
+
+    formdata3 = data_class()
+    formdata3.data = {
+        '8': {
+            'data': [{'123': 'blah'}],
+            'schema': {},  # not important here
+        },
+        '8_display': 'blah',
+    }
+    formdata3.just_created()
+    formdata3.jump_status('new')
+    formdata3.store()
+
+    resp = app.get('/backoffice/management/form-title/')
+    assert [x.text_content() for x in resp.pyquery('#columns-filter label')] == [
+        'Number',
+        'Created',
+        'Last Modified',
+        '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 data-field-sort-key="f8-123"><span>Block / Test</span></th>' in resp
+    assert '<th data-field-sort-key="f8-456"><span>Block / card field</span></th>' in resp
+    assert resp.text.count('<tr') == 4
+
+    # XXX ordering is done on card id, to be fixed with #60742
+    resp = app.get('/backoffice/management/form-title/?order_by=f8-456')
+    ids = [int(x.strip('/')) for x in re.findall(r'data-link="(.*?)"', resp.text)]
+    assert ids == [formdata1.id, formdata2.id, formdata3.id]
+
+    resp = app.get('/backoffice/management/form-title/?order_by=-f8-456')
+    ids = [int(x.strip('/')) for x in re.findall(r'data-link="(.*?)"', resp.text)]
+    assert ids == [formdata3.id, formdata2.id, formdata1.id]
diff --git a/wcs/forms/backoffice.py b/wcs/forms/backoffice.py
index 19ff2eb1..84e94ae5 100644
--- a/wcs/forms/backoffice.py
+++ b/wcs/forms/backoffice.py
@@ -111,7 +111,10 @@ class FormDefUI:
                     field_sort_key = 'receipt_time'
                 elif f.id in ('user-label', 'submission_agent'):
                     field_sort_key = None
-            elif getattr(f, 'is_related_field', False) or getattr(f, 'block_field', None):
+            elif getattr(f, 'is_related_field', False):
+                field_sort_key = None
+            elif getattr(f, 'block_field', None) and f.block_field.max_items != 1:
+                # allow sorting on a field of block field if there is one item only
                 field_sort_key = None
             else:
                 field_sort_key = 'f%s' % f.contextual_id
@@ -213,6 +216,32 @@ class FormDefUI:
         if order_by and not hasattr(formdata_class, 'get_sorted_ids'):
             # get_sorted_ids is only implemented in the SQL backend
             order_by = None
+
+        def iter_fields(fields, block_field=None):
+            for field in fields:
+                field.contextual_id = field.id
+                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)
+                yield field
+                if field.key == 'block':
+                    yield from iter_fields(field.block.fields, block_field=field)
+                    continue
+
+        direction = ''
+        if order_by.startswith('-'):
+            order_by = order_by[1:]
+            direction = '-'
+        for field in iter_fields(self.formdef.get_all_fields()):
+            if getattr(field, 'block_field', None) and 'f%s' % field.contextual_id == order_by:
+                # field of block field, sort on the first element
+                order_by = "f%s->'data'->0->>'%s'" % (field.block_field.id, field.id)
+
+        order_by = '%s%s' % (direction, order_by)
+
         if order_by and not anonymise:
             ordered_ids = formdata_class.get_sorted_ids(order_by, clause=criterias)
             item_ids_dict = {x: True for x in item_ids}
diff --git a/wcs/sql.py b/wcs/sql.py
index 4d6967f7..bb229a38 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -1654,10 +1654,16 @@ class SqlMixin:
         if order_by.startswith('-'):
             order_by = order_by[1:]
             direction = 'DESC'
-        order_by = order_by.replace('-', '_')
+        if '->' in order_by:
+            # sort on field of block field: f42->'data'->0->>'bf13e4d8a8-fb08-4808-b5ae-02d6247949b9'
+            parts = order_by.split('->')
+            order_by = '%s->%s' % (parts[0].replace('-', '_'), '->'.join(parts[1:]))
+        else:
+            order_by = order_by.replace('-', '_')
 
         fields = [x[0] for x in cls._table_static_fields] + cls.get_data_fields()
-        if order_by not in fields:
+        if order_by.split('->')[0] not in fields:
+            # for a sort on field of block field, just check the existence of the block field
             return ''
 
         return ' ORDER BY %s %s' % (order_by, direction)
-- 
2.34.1

