From 158fdc25d6b5cbec2b52934fcb07ab521b0e63ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 20 May 2022 15:15:26 +0200
Subject: [PATCH 2/4] api/backoffice: order by rank (#65511)

---
 tests/api/test_formdata.py | 51 ++++++++++++++++++++++++++++++++++++++
 wcs/forms/backoffice.py    |  7 +++++-
 wcs/sql.py                 | 10 +++++++-
 3 files changed, 66 insertions(+), 2 deletions(-)

diff --git a/tests/api/test_formdata.py b/tests/api/test_formdata.py
index 152449ea6..7abae92ae 100644
--- a/tests/api/test_formdata.py
+++ b/tests/api/test_formdata.py
@@ -814,6 +814,57 @@ def test_api_list_formdata(pub, local_user):
         assert len(resp.json) == 14
 
 
+def test_api_list_formdata_order_by_rank(pub, local_user):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+    pub.role_class.wipe()
+    role = pub.role_class(name='test')
+    role.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_roles = {'_receiver': role.id}
+    formdef.fields = [
+        fields.StringField(id='0', label='a', type='string', display_locations=['listings']),
+        fields.StringField(id='1', label='b', type='string'),
+    ]
+    formdef.store()
+
+    data_class = formdef.data_class()
+    data_class.wipe()
+
+    # 1st formdata with foo in "unimportant" field
+    formdata1 = data_class()
+    formdata1.data = {'1': 'FOO'}
+    formdata1.just_created()
+    formdata1.jump_status('new')
+    formdata1.store()
+
+    # 2nd formdata, with no foo
+    formdata2 = data_class()
+    formdata2.data = {}
+    formdata2.just_created()
+    formdata2.jump_status('new')
+    formdata2.store()
+
+    # 3rd formdata, with foo in "important" field
+    formdata3 = data_class()
+    formdata3.data = {'0': 'FOO'}
+    formdata3.just_created()
+    formdata3.jump_status('new')
+    formdata3.store()
+
+    # add proper role to user
+    local_user.roles = [role.id]
+    local_user.store()
+
+    # check fts
+    resp = get_app(pub).get(sign_uri('/api/forms/test/list?full=on&q=foo', user=local_user))
+    assert len(resp.json) == 2
+    assert [int(x['id']) for x in resp.json] == [formdata3.id, formdata1.id]
+
+
 def test_api_list_formdata_unknown_filter(pub, local_user):
     pub.role_class.wipe()
     role = pub.role_class(name='test')
diff --git a/wcs/forms/backoffice.py b/wcs/forms/backoffice.py
index 7f6844a34..7672fc782 100644
--- a/wcs/forms/backoffice.py
+++ b/wcs/forms/backoffice.py
@@ -19,7 +19,7 @@ import urllib.parse
 from quixote import get_publisher, get_request, get_session, redirect
 from quixote.html import TemplateIO, htmltext
 
-from wcs.qommon.storage import Contains, NotContains, Null, StrictNotEqual
+from wcs.qommon.storage import Contains, FtsMatch, NotContains, Null, StrictNotEqual
 from wcs.roles import logged_users_role
 
 from ..qommon import _, misc
@@ -306,6 +306,11 @@ class FormDefUI:
             ordered_ids = formdata_class.get_sorted_ids(order_by, clause=criterias)
             item_ids_dict = {x: True for x in item_ids}
             item_ids = [x for x in ordered_ids if x in item_ids_dict]
+        elif not anonymise and get_publisher().is_using_postgresql() and query:
+            criterias = [FtsMatch(query)] + (criterias or [])
+            ordered_ids = formdata_class.get_sorted_ids('rank', clause=criterias)
+            item_ids_dict = {x: True for x in item_ids}
+            item_ids = [x for x in ordered_ids if x in item_ids_dict]
         else:
             item_ids.sort(key=int)
             item_ids.reverse()
diff --git a/wcs/sql.py b/wcs/sql.py
index 5ce4c6aa4..f861f7901 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -2361,7 +2361,15 @@ class SqlMixin:
         assert not func_clause
         if where_clauses:
             sql_statement += ' WHERE ' + ' AND '.join(where_clauses)
-        sql_statement += cls.get_order_by_clause(order_by)
+        if order_by == 'rank':
+            try:
+                fts = [x for x in clause if not callable(x) and x.__class__.__name__ == 'FtsMatch'][0]
+            except IndexError:
+                pass
+            else:
+                sql_statement += ' ORDER BY ts_rank(fts, plainto_tsquery(%%(c%s)s)) DESC' % id(fts.value)
+        else:
+            sql_statement += cls.get_order_by_clause(order_by)
         cur.execute(sql_statement, parameters)
         ids = [x[0] for x in cur.fetchall()]
         conn.commit()
-- 
2.36.1

