From 910d6c436cbed3946a34b9dc8db28e62e22e7138 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Tue, 3 May 2022 20:05:06 +0200
Subject: [PATCH] api: add suppot for statistics on cards (#62029)

---
 tests/api/test_statistics.py | 25 +++++++++++-
 wcs/sql.py                   | 11 ++++--
 wcs/statistics/views.py      | 73 ++++++++++++++++++++++++++++++++----
 wcs/urls.py                  |  5 +++
 4 files changed, 103 insertions(+), 11 deletions(-)

diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py
index cda56ce4d..0e8829be2 100644
--- a/tests/api/test_statistics.py
+++ b/tests/api/test_statistics.py
@@ -5,6 +5,7 @@ import pytest
 
 from wcs import fields
 from wcs.blocks import BlockDef
+from wcs.carddef import CardDef
 from wcs.categories import Category
 from wcs.formdef import FormDef
 from wcs.qommon.http_request import HTTPRequest
@@ -129,7 +130,7 @@ def test_statistics_index_forms(pub):
     resp = get_app(pub).get(sign_uri('/api/statistics/'))
     form_filter = [x for x in resp.json['data'][0]['filters'] if x['id'] == 'form'][0]
     assert form_filter['options'] == [
-        {'id': '_all', 'label': 'All'},
+        {'id': '_all', 'label': 'All Forms'},
         {'id': 'test-1', 'label': 'test 1'},
         {'id': 'test-2', 'label': 'test 2'},
     ]
@@ -569,3 +570,25 @@ def test_statistics_forms_count_group_by(pub, formdef, anonymise):
     # invalid field
     resp = get_app(pub).get(sign_uri(url + '&group-by=xxx'))
     assert resp.json['data']['series'] == [{'data': [16, 0, 4], 'label': 'Forms Count'}]
+
+
+def test_statistics_cards_count(pub):
+    carddef = CardDef()
+    carddef.name = 'test 1'
+    carddef.fields = []
+    carddef.store()
+    carddef.data_class().wipe()
+
+    for _i in range(20):
+        carddata = carddef.data_class()()
+        carddata.just_created()
+        carddata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple()
+        carddata.store()
+
+    # apply (required) card filter
+    resp = get_app(pub).get(sign_uri('/api/statistics/cards/count/?form=%s' % carddef.url_name))
+    assert resp.json['data']['series'] == [{'data': [20], 'label': 'Cards Count'}]
+    assert resp.json['data']['x_labels'] == ['2021-01']
+
+    resp = get_app(pub).get(sign_uri('/api/statistics/cards/count/?card=%s' % 'invalid'), status=400)
+    assert resp.text == 'invalid form'
diff --git a/wcs/sql.py b/wcs/sql.py
index ba049de6b..f4f2740a9 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -3801,13 +3801,18 @@ def get_period_query(
     clause = [NotNull('receipt_time')]
     table_name = 'wcs_all_forms'
     if criterias:
+        from wcs.formdef import FormDef
+
+        formdef_class = FormDef
         for criteria in criterias:
+            if criteria.__class__.__name__ == 'Equal' and criteria.attribute == 'formdef_klass':
+                formdef_class = criteria.value
+                continue
+
             if criteria.__class__.__name__ == 'Equal' and criteria.attribute == 'formdef_id':
                 # if there's a formdef_id specified, switch to using the
                 # specific table so we have access to all fields
-                from wcs.formdef import FormDef
-
-                table_name = get_formdef_table_name(FormDef.get(criteria.value))
+                table_name = get_formdef_table_name(formdef_class.get(criteria.value))
                 continue
             clause.append(criteria)
     if period_start:
diff --git a/wcs/statistics/views.py b/wcs/statistics/views.py
index 215a36053..e67af8ca8 100644
--- a/wcs/statistics/views.py
+++ b/wcs/statistics/views.py
@@ -23,7 +23,9 @@ from quixote import get_publisher
 
 from wcs import sql
 from wcs.api_utils import is_url_signed
+from wcs.backoffice.data_management import CardPage
 from wcs.backoffice.management import FormPage
+from wcs.carddef import CardDef
 from wcs.categories import Category
 from wcs.formdef import FormDef
 from wcs.qommon import _, misc
@@ -47,11 +49,14 @@ class IndexView(RestrictedView):
         category_options = [{'id': '_all', 'label': C_('categories|All')}] + [
             {'id': x.url_name, 'label': x.name} for x in categories
         ]
-        forms = FormDef.select()
+        forms = FormDef.select(lightweight=True)
         forms.sort(key=lambda x: misc.simplify(x.name))
-        form_options = [{'id': '_all', 'label': _('All')}] + [
+        form_options = [{'id': '_all', 'label': _('All Forms')}] + [
             {'id': x.url_name, 'label': x.name} for x in forms
         ]
+        cards = CardDef.select(lightweight=True)
+        cards.sort(key=lambda x: misc.simplify(x.name))
+        card_options = [{'id': x.url_name, 'label': x.name} for x in cards]
         return JsonResponse(
             {
                 'data': [
@@ -100,13 +105,57 @@ class IndexView(RestrictedView):
                                 'has_subfilters': True,
                             },
                         ],
-                    }
+                    },
+                    {
+                        'name': _('Cards Count'),
+                        'url': request.build_absolute_uri(reverse('api-statistics-cards-count')),
+                        'id': 'cards_counts',
+                        'filters': [
+                            {
+                                'id': 'time_interval',
+                                'label': _('Interval'),
+                                'options': [
+                                    {
+                                        'id': 'month',
+                                        'label': _('Month'),
+                                    },
+                                    {
+                                        'id': 'year',
+                                        'label': _('Year'),
+                                    },
+                                    {
+                                        'id': 'weekday',
+                                        'label': _('Week day'),
+                                    },
+                                    {
+                                        'id': 'hour',
+                                        'label': _('Hour'),
+                                    },
+                                ],
+                                'required': True,
+                                'default': 'month',
+                            },
+                            {
+                                'id': 'form',
+                                'label': _('Card'),
+                                'options': card_options,
+                                'required': True,
+                                'default': '',
+                                'has_subfilters': True,
+                            },
+                        ],
+                    },
                 ]
             }
         )
 
 
 class FormsCountView(RestrictedView):
+    formdef_class = FormDef
+    formpage_class = FormPage
+    has_global_count_support = True
+    label = _('Forms Count')
+
     def get(self, request, *args, **kwargs):
         time_interval = request.GET.get('time_interval', 'month')
         totals_kwargs = {
@@ -115,16 +164,19 @@ class FormsCountView(RestrictedView):
             'criterias': [],
         }
         category_slug = request.GET.get('category', '_all')
-        formdef_slug = request.GET.get('form', '_all')
+        formdef_slug = request.GET.get('form', '_all' if self.has_global_count_support else '_nothing')
         group_by = request.GET.get('group-by')
         subfilters = []
         if formdef_slug != '_all':
             try:
-                formdef = FormDef.get_by_urlname(formdef_slug, ignore_migration=True)
+                formdef = self.formdef_class.get_by_urlname(formdef_slug, ignore_migration=True)
             except KeyError:
                 return HttpResponseBadRequest('invalid form')
-            form_page = FormPage(formdef=formdef, update_breadcrumbs=False)
+            form_page = self.formpage_class(formdef=formdef, update_breadcrumbs=False)
 
+            # formdef_klass is a fake criteria, it will be used in time interval functions
+            # to switch to appropriate class, it must appear before formdef_id.
+            totals_kwargs['criterias'].append(Equal('formdef_klass', self.formdef_class))
             totals_kwargs['criterias'].append(Equal('formdef_id', formdef.id))
             totals_kwargs['criterias'].extend(self.get_filters_criterias(formdef, form_page))
             if group_by:
@@ -161,7 +213,7 @@ class FormsCountView(RestrictedView):
 
         if 'group_by' not in totals_kwargs:
             x_labels = [x[0] for x in totals]
-            series = [{'label': _('Forms Count'), 'data': [x[1] for x in totals]}]
+            series = [{'label': self.label, 'data': [x[1] for x in totals]}]
         else:
             x_labels, series = self.get_grouped_data(totals, group_by_field, formdef, form_page)
 
@@ -307,3 +359,10 @@ class FormsCountView(RestrictedView):
         ]
         series.sort(key=lambda x: x['label'].lower())
         return x_labels, series
+
+
+class CardsCountView(FormsCountView):
+    formdef_class = CardDef
+    formpage_class = CardPage
+    has_global_count_support = False
+    label = _('Cards Count')
diff --git a/wcs/urls.py b/wcs/urls.py
index 1ff525732..da6375cc8 100644
--- a/wcs/urls.py
+++ b/wcs/urls.py
@@ -51,6 +51,11 @@ urlpatterns = [
         statistics_views.FormsCountView.as_view(),
         name='api-statistics-forms-count',
     ),
+    url(
+        r'^api/statistics/cards/count/$',
+        statistics_views.CardsCountView.as_view(),
+        name='api-statistics-cards-count',
+    ),
     # provide django.contrib.auth view names for compatibility with
     # templates created for classic django applications.
     url(r'^login/$', compat.quixote, name='auth_login'),
-- 
2.36.0

