From d289ae8bee3bfe9b75d9130f92f02322f0945a31 Mon Sep 17 00:00:00 2001
From: Valentin Deniaud <vdeniaud@entrouvert.com>
Date: Wed, 11 May 2022 15:50:25 +0200
Subject: [PATCH] statistics: merge category selection into form options
 (#62532)

---
 tests/api/test_statistics.py | 87 ++++++++++++++++++++++++++++++++++--
 wcs/statistics/views.py      | 49 ++++++++++++++++----
 2 files changed, 125 insertions(+), 11 deletions(-)

diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py
index 0e8829be2..e91b61fcf 100644
--- a/tests/api/test_statistics.py
+++ b/tests/api/test_statistics.py
@@ -6,7 +6,7 @@ import pytest
 from wcs import fields
 from wcs.blocks import BlockDef
 from wcs.carddef import CardDef
-from wcs.categories import Category
+from wcs.categories import CardDefCategory, Category
 from wcs.formdef import FormDef
 from wcs.qommon.http_request import HTTPRequest
 from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
@@ -112,6 +112,7 @@ def test_statistics_index_categories(pub):
         {'id': 'category-a', 'label': 'Category A'},
         {'id': 'category-b', 'label': 'Category B'},
     ]
+    assert category_filter['deprecated'] is True
 
 
 def test_statistics_index_forms(pub):
@@ -135,6 +136,82 @@ def test_statistics_index_forms(pub):
         {'id': 'test-2', 'label': 'test 2'},
     ]
 
+    category_a = Category(name='Category A')
+    category_a.store()
+    category_b = Category(name='Category B')
+    category_b.store()
+    formdef2.category_id = category_a.id
+    formdef2.store()
+
+    formdef3 = FormDef()
+    formdef3.name = 'test 3'
+    formdef3.category_id = category_b.id
+    formdef3.store()
+    formdef3.data_class().wipe()
+
+    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'] == [
+        [None, [{'id': '_all', 'label': 'All Forms'}]],
+        [
+            'Category A',
+            [
+                {'id': 'category:category-a', 'label': 'All forms of category Category A'},
+                {'id': 'test-2', 'label': 'test 2'},
+            ],
+        ],
+        [
+            'Category B',
+            [
+                {'id': 'category:category-b', 'label': 'All forms of category Category B'},
+                {'id': 'test-3', 'label': 'test 3'},
+            ],
+        ],
+        ['Misc', [{'id': 'test-1', 'label': 'test 1'}]],
+    ]
+
+
+def test_statistics_index_cards(pub):
+    carddef = CardDef()
+    carddef.name = 'test 1'
+    carddef.fields = []
+    carddef.store()
+    carddef.data_class().wipe()
+
+    carddef2 = CardDef()
+    carddef2.name = 'test 2'
+    carddef2.fields = []
+    carddef2.store()
+    carddef2.data_class().wipe()
+
+    resp = get_app(pub).get(sign_uri('/api/statistics/'))
+    form_filter = [x for x in resp.json['data'][1]['filters'] if x['id'] == 'form'][0]
+    assert form_filter['options'] == [
+        {'id': 'test-1', 'label': 'test 1'},
+        {'id': 'test-2', 'label': 'test 2'},
+    ]
+
+    category_a = CardDefCategory(name='Category A')
+    category_a.store()
+    category_b = CardDefCategory(name='Category B')
+    category_b.store()
+    carddef2.category_id = category_a.id
+    carddef2.store()
+
+    carddef3 = CardDef()
+    carddef3.name = 'test 3'
+    carddef3.category_id = category_b.id
+    carddef3.store()
+    carddef3.data_class().wipe()
+
+    resp = get_app(pub).get(sign_uri('/api/statistics/'))
+    form_filter = [x for x in resp.json['data'][1]['filters'] if x['id'] == 'form'][0]
+    assert form_filter['options'] == [
+        ['Category A', [{'id': 'test-2', 'label': 'test 2'}]],
+        ['Category B', [{'id': 'test-3', 'label': 'test 3'}]],
+        ['Misc', [{'id': 'test-1', 'label': 'test 1'}]],
+    ]
+
 
 def test_statistics_forms_count(pub):
     category_a = Category(name='Category A')
@@ -222,8 +299,8 @@ def test_statistics_forms_count(pub):
     # time_interval=day is not supported
     get_app(pub).get(sign_uri('/api/statistics/forms/count/?time_interval=day'), status=400)
 
-    # apply category filter
-    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?category=category-a'))
+    # apply category filter through form parameter
+    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?form=category:category-a'))
     assert resp.json == {
         'data': {
             'series': [{'data': [20], 'label': 'Forms Count'}],
@@ -233,6 +310,10 @@ def test_statistics_forms_count(pub):
         'err': 0,
     }
 
+    # apply category filter (legacy)
+    new_resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?category=category-a'))
+    assert new_resp.json == resp.json
+
     # apply category id filter (legacy)
     new_resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?category=%s' % category_a.id))
     assert new_resp.json == resp.json
diff --git a/wcs/statistics/views.py b/wcs/statistics/views.py
index e67af8ca8..0a1375be6 100644
--- a/wcs/statistics/views.py
+++ b/wcs/statistics/views.py
@@ -44,16 +44,13 @@ class IndexView(RestrictedView):
     def get(self, request, *args, **kwargs):
         if not get_publisher().is_using_postgresql():
             return JsonResponse({'data': [], 'err': 0})
+
         categories = Category.select()
         categories.sort(key=lambda x: misc.simplify(x.name))
         category_options = [{'id': '_all', 'label': C_('categories|All')}] + [
             {'id': x.url_name, 'label': x.name} for x in categories
         ]
-        forms = FormDef.select(lightweight=True)
-        forms.sort(key=lambda x: misc.simplify(x.name))
-        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]
@@ -95,11 +92,15 @@ class IndexView(RestrictedView):
                                 'options': category_options,
                                 'required': True,
                                 'default': '_all',
+                                'deprecated': True,
+                                'deprecation_hint': _(
+                                    'Category should now be selected using the Form field below.'
+                                ),
                             },
                             {
                                 'id': 'form',
                                 'label': _('Form'),
-                                'options': form_options,
+                                'options': self.get_form_options(FormDef),
                                 'required': True,
                                 'default': '_all',
                                 'has_subfilters': True,
@@ -138,7 +139,7 @@ class IndexView(RestrictedView):
                             {
                                 'id': 'form',
                                 'label': _('Card'),
-                                'options': card_options,
+                                'options': self.get_form_options(CardDef, include_all_option=False),
                                 'required': True,
                                 'default': '',
                                 'has_subfilters': True,
@@ -149,6 +150,36 @@ class IndexView(RestrictedView):
             }
         )
 
+    @staticmethod
+    def get_form_options(formdef_class, include_all_option=True):
+        all_forms_option = [{'id': '_all', 'label': _('All Forms')}] if include_all_option else []
+
+        forms = formdef_class.select(lightweight=True)
+        forms.sort(key=lambda x: misc.simplify(x.name))
+        forms_with_category = [x for x in forms if x.category]
+        if not forms_with_category:
+            return all_forms_option + [{'id': x.url_name, 'label': x.name} for x in forms]
+
+        form_options = collections.defaultdict(list)
+        for x in forms_with_category:
+            if x.category.name not in form_options and include_all_option:
+                form_options[x.category.name] = [
+                    {
+                        'id': 'category:' + x.category.url_name,
+                        'label': _('All forms of category %s') % x.category.name,
+                    }
+                ]
+            form_options[x.category.name].append({'id': x.url_name, 'label': x.name})
+        form_options = sorted(
+            ((category, forms) for category, forms in form_options.items()), key=lambda x: misc.simplify(x[0])
+        )
+
+        forms_without_category_options = [
+            {'id': x.url_name, 'label': x.name} for x in forms if not x.category
+        ]
+        all_forms_option_group = [(None, all_forms_option)] if include_all_option else []
+        return all_forms_option_group + form_options + [(_('Misc'), forms_without_category_options)]
+
 
 class FormsCountView(RestrictedView):
     formdef_class = FormDef
@@ -167,7 +198,7 @@ class FormsCountView(RestrictedView):
         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':
+        if formdef_slug != '_all' and not formdef_slug.startswith('category:'):
             try:
                 formdef = self.formdef_class.get_by_urlname(formdef_slug, ignore_migration=True)
             except KeyError:
@@ -189,6 +220,8 @@ class FormsCountView(RestrictedView):
         else:
             totals_kwargs['criterias'].append(StrictNotEqual('status', 'draft'))
 
+            if formdef_slug.startswith('category:'):
+                category_slug = formdef_slug.split(':', 1)[1]
             if category_slug != '_all':
                 try:
                     category = Category.get_by_urlname(category_slug)
-- 
2.30.2

