From 4e431eb7ccb97d6b7539fdf057a397fd7531d4ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 7 Dec 2020 09:50:04 +0100
Subject: [PATCH] api: new formdef popularity count method (#47889)

---
 tests/test_api.py | 37 +++++++++++++++++++++++++++++++++++++
 wcs/api.py        | 27 +++++++++++++++++++++++++--
 wcs/sql.py        | 39 ++++++++++++++++++++++++++++++++-------
 3 files changed, 94 insertions(+), 9 deletions(-)

diff --git a/tests/test_api.py b/tests/test_api.py
index 1442c7ba..61abc798 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -420,6 +420,43 @@ def test_formdef_list(pub):
     assert resp1.json['err'] == 0
     assert len(resp1.json['data']) == 0
 
+    formdef.data_class().wipe()
+
+    # a draft
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.just_created()
+    formdata.status = 'draft'
+    formdata.store()
+
+    other_formdef = FormDef()
+    other_formdef.name = 'test 2'
+    other_formdef.fields = []
+    other_formdef.store()
+    other_formdata = other_formdef.data_class()()
+    other_formdata.data = {}
+    other_formdata.just_created()
+    other_formdata.store()
+
+    # formdata created:
+    # - 1 day ago (=3*4)
+    # - 7 days ago (=2*2)
+    # - 29 days ago (=1*1)
+    # - 31 days ago (=0)
+    for days in [1, 1, 1, 7, 7, 29, 31]:
+        formdata = formdef.data_class()()
+        formdata.data = {}
+        formdata.just_created()
+        formdata.receipt_time = (datetime.datetime.now() - datetime.timedelta(days=days)).timetuple()
+        formdata.store()
+
+    resp = get_app(pub).get(sign_uri('/api/formdefs/?include-count=on'))
+    if not pub.is_using_postgresql():
+        assert resp.json['data'][0]['count'] == 8
+    else:
+        # 3*4 + 2*2 + 1*1
+        assert resp.json['data'][0]['count'] == 17
+
 
 def test_limited_formdef_list(pub, local_user):
     Role.wipe()
diff --git a/wcs/api.py b/wcs/api.py
index 2763f385..b720b731 100644
--- a/wcs/api.py
+++ b/wcs/api.py
@@ -14,12 +14,13 @@
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+import datetime
 import json
 import re
 import time
 
 from quixote import get_request, get_publisher, get_response, get_session
-from quixote.errors import MethodNotAllowedError, QueryError
+from quixote.errors import MethodNotAllowedError
 from quixote.directory import Directory
 
 from django.utils.encoding import force_text
@@ -573,7 +574,29 @@ class ApiFormdefsDirectory(Directory):
             if include_count:
                 # we include the count of submitted forms so it's possible to sort
                 # them by "popularity"
-                formdict['count'] = formdef.data_class().count()
+                if get_publisher().is_using_postgresql():
+                    from wcs import sql
+                    # 4 * number of submitted forms of last 2 days
+                    # + 2 * number of submitted forms of last 8 days
+                    # + 1 * number of submitted forms of last 30 days
+                    # exclude drafts
+                    criterias = [Equal('formdef_id', formdef.id), NotEqual('status', 'draft')]
+                    d_now = datetime.datetime.now()
+                    count = 4 * sql.get_period_total(
+                        period_start=d_now - datetime.timedelta(days=2), include_start=True,
+                        criterias=criterias)
+                    count += 2 * sql.get_period_total(
+                        period_start=d_now - datetime.timedelta(days=8), include_start=True,
+                        period_end=d_now - datetime.timedelta(days=2), include_end=False,
+                        criterias=criterias)
+                    count += sql.get_period_total(
+                        period_start=d_now - datetime.timedelta(days=30), include_start=True,
+                        period_end=d_now - datetime.timedelta(days=8), include_end=False,
+                        criterias=criterias)
+                else:
+                    # naive count
+                    count = formdef.data_class().count()
+                formdict['count'] = count
 
             formdict['functions'] = {}
             formdef_workflow_roles = formdef.workflow_roles or {}
diff --git a/wcs/sql.py b/wcs/sql.py
index 57f37fda..3f215173 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -2525,7 +2525,7 @@ class AnyFormData(SqlMixin):
             formdatas[0].load_all_evolutions(formdatas)
 
 
-def get_period_query(period_start=None, period_end=None, criterias=None, parameters=None):
+def get_period_query(period_start=None, include_start=True, period_end=None, include_end=True, criterias=None, parameters=None):
     clause = [NotNull('receipt_time')]
     table_name = 'wcs_all_forms'
     if criterias:
@@ -2539,9 +2539,15 @@ def get_period_query(period_start=None, period_end=None, criterias=None, paramet
                 continue
             clause.append(criteria)
     if period_start:
-        clause.append(GreaterOrEqual('receipt_time', period_start))
+        if include_start:
+            clause.append(GreaterOrEqual('receipt_time', period_start))
+        else:
+            clause.append(Greater('receipt_time', period_start))
     if period_end:
-        clause.append(LessOrEqual('receipt_time', period_end))
+        if include_end:
+            clause.append(LessOrEqual('receipt_time', period_end))
+        else:
+            clause.append(Less('receipt_time', period_end))
     where_clauses, params, func_clause = parse_clause(clause)
     parameters.update(params)
     statement = ' FROM %s ' % table_name
@@ -2589,7 +2595,7 @@ def get_weekday_totals(period_start=None, period_end=None, criterias=None):
     conn, cur = get_connection_and_cursor()
     statement = '''SELECT DATE_PART('dow', receipt_time) AS weekday, COUNT(*)'''
     parameters = {}
-    statement += get_period_query(period_start, period_end, criterias, parameters)
+    statement += get_period_query(period_start=period_start, period_end=period_end, criterias=criterias, parameters=parameters)
     statement += ' GROUP BY weekday ORDER BY weekday'''
     cur.execute(statement, parameters)
 
@@ -2612,7 +2618,7 @@ def get_hour_totals(period_start=None, period_end=None, criterias=None):
     conn, cur = get_connection_and_cursor()
     statement = '''SELECT DATE_PART('hour', receipt_time) AS hour, COUNT(*)'''
     parameters = {}
-    statement += get_period_query(period_start, period_end, criterias, parameters)
+    statement += get_period_query(period_start=period_start, period_end=period_end, criterias=criterias, parameters=parameters)
     statement += ' GROUP BY hour ORDER BY hour'
     cur.execute(statement, parameters)
 
@@ -2636,7 +2642,7 @@ def get_monthly_totals(period_start=None, period_end=None, criterias=None):
     conn, cur = get_connection_and_cursor()
     statement = '''SELECT DATE_TRUNC('month', receipt_time) AS month, COUNT(*) '''
     parameters = {}
-    statement += get_period_query(period_start, period_end, criterias, parameters)
+    statement += get_period_query(period_start=period_start, period_end=period_end, criterias=criterias, parameters=parameters)
     statement += ' GROUP BY month ORDER BY month'''
     cur.execute(statement, parameters)
 
@@ -2665,7 +2671,7 @@ def get_yearly_totals(period_start=None, period_end=None, criterias=None):
     conn, cur = get_connection_and_cursor()
     statement = '''SELECT DATE_TRUNC('year', receipt_time) AS year, COUNT(*)'''
     parameters = {}
-    statement += get_period_query(period_start, period_end, criterias, parameters)
+    statement += get_period_query(period_start=period_start, period_end=period_end, criterias=criterias, parameters=parameters)
     statement += ' GROUP BY year ORDER BY year'
     cur.execute(statement, parameters)
 
@@ -2688,6 +2694,25 @@ def get_yearly_totals(period_start=None, period_end=None, criterias=None):
     return result
 
 
+@guard_postgres
+def get_period_total(period_start=None, include_start=True, period_end=None, include_end=True, criterias=None):
+    conn, cur = get_connection_and_cursor()
+    statement = '''SELECT COUNT(*)'''
+    parameters = {}
+    statement += get_period_query(
+        period_start=period_start, include_start=include_start,
+        period_end=period_end, include_end=include_end,
+        criterias=criterias, parameters=parameters)
+    cur.execute(statement, parameters)
+
+    result = int(cur.fetchone()[0])
+
+    conn.commit()
+    cur.close()
+
+    return result
+
+
 # latest migration, number + description (description is not used
 # programmaticaly but will make sure git conflicts if two migrations are
 # separately added with the same number)
-- 
2.28.0

