From ad0f4413d3a5084888e92551ca09ed16fe179cad Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 17 Jan 2023 18:05:06 +0100 Subject: [PATCH 1/4] tests: add some new statistics tests (#73770) --- tests/api/test_statistics.py | 40 ++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py index 7d74dbd14..ac641676c 100644 --- a/tests/api/test_statistics.py +++ b/tests/api/test_statistics.py @@ -563,6 +563,23 @@ def test_statistics_forms_count_subfilters(pub, formdef): new_resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?form=%s' % formdef.url_name)) assert new_resp.json == resp.json + # add items field inside block field, it should not appear + items_field = fields.ItemsField( + id='2', + varname='items', + label='Block items', + type='items', + items=['foo', 'bar', 'baz'], + anonymise=False, + display_locations=['statistics'], + ) + formdef.fields[2].block.fields.append(bool_field) + formdef.store() + formdata.data['4'] = {'data': [{'2': ['bar']}]} + formdata.store() + new_resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?form=%s' % formdef.url_name)) + assert new_resp.json == resp.json + # remove fields and statuses workflow = Workflow(name='Empty wf') workflow.store() @@ -587,7 +604,7 @@ def test_statistics_forms_count_subfilters_query(pub, formdef): formdata.data['1'] = True formdata.data['2'] = 'foo' formdata.data['3'] = ['bar', 'baz'] - formdata.data['4'] = {'data': [{'1': True}]} + formdata.data['4'] = {'data': [{'1': True}, {'1': False}]} elif i % 2: formdata.data['1'] = False formdata.data['2'] = 'baz' @@ -646,7 +663,7 @@ def test_statistics_forms_count_subfilters_query(pub, formdef): assert resp.json['data']['series'][0]['data'][0] == 13 resp = get_app(pub).get(sign_uri(url + '&filter-blockdata_bool=false')) - assert resp.json['data']['series'][0]['data'][0] == 3 + assert resp.json['data']['series'][0]['data'][0] == 16 # filter on status resp = get_app(pub).get(sign_uri(url + '&filter-status=_all')) @@ -718,6 +735,25 @@ def test_statistics_forms_count_subfilters_query_same_varname(pub, formdef): assert resp.json['data']['series'] == [{'data': [], 'label': 'Forms Count'}] +def test_statistics_forms_count_subfilters_query_integer_items(pub, formdef): + for i in range(10): + formdata = formdef.data_class()() + formdata.just_created() + if i % 2: + formdata.data['3'] = ['1', '2'] + else: + formdata.data['3'] = ['1'] + formdata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple() + formdata.store() + + url = '/api/statistics/forms/count/?form=%s' % formdef.url_name + resp = get_app(pub).get(sign_uri(url + '&filter-test-items=1')) + assert resp.json['data']['series'][0]['data'][0] == 10 + + resp = get_app(pub).get(sign_uri(url + '&filter-test-items=2')) + assert resp.json['data']['series'][0]['data'][0] == 5 + + @pytest.mark.parametrize('anonymise', [False, True]) def test_statistics_forms_count_group_by(pub, formdef, anonymise): for i in range(20): -- 2.39.2 From a85d88ef350ea61a60c51442adb09a45a8383296 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 16 Jan 2023 16:09:42 +0100 Subject: [PATCH 2/4] formdata: aggregate field data for statistics in new column (#73770) --- wcs/formdata.py | 33 +++++++++++++++++++++++++++++++-- wcs/sql.py | 24 +++++++++++++++++------- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/wcs/formdata.py b/wcs/formdata.py index 14dea17bc..06971a231 100644 --- a/wcs/formdata.py +++ b/wcs/formdata.py @@ -302,6 +302,7 @@ class FormData(StorableObject): workflow_data = None workflow_roles = None geolocations = None + statistics_data = None _formdef = None @@ -521,8 +522,9 @@ class FormData(StorableObject): changed = False - def get_all_fields(): - for field in self.formdef.fields: + def get_all_fields(with_backoffice_fields=False): + fields = self.formdef.get_all_fields() if with_backoffice_fields else self.formdef.fields + for field in fields: yield field if field.key == 'block': try: @@ -608,6 +610,33 @@ class FormData(StorableObject): if digests: self.digests = digests changed = True + + new_statistics_data = {} + for field in get_all_fields(with_backoffice_fields=True): + if 'statistics' not in field.display_locations: + continue + + if new_statistics_data.get(field.varname): + continue # ignore fields with duplicated varname if we already have data + + block = getattr(field, 'block', None) + if block: + sub_data = self.data.get(block.id) or {} + items = set() + for data in sub_data.get('data', []): + value = data.get(field.id) + items.add(value) + values = list(items) + else: + values = self.data.get(field.id) + if not isinstance(values, list): + values = [values] + new_statistics_data[field.varname] = [x for x in values if x is not None] + + if new_statistics_data != self.statistics_data: + self.statistics_data = new_statistics_data + changed = True + return changed def get_lateral_block(self): diff --git a/wcs/sql.py b/wcs/sql.py index fc9fdbbf4..f7024ba04 100644 --- a/wcs/sql.py +++ b/wcs/sql.py @@ -925,7 +925,8 @@ BEGIN NEW.criticality_level - {criticality_levels}, {geoloc_base_x}, {geoloc_base_y}, - NEW.anonymised); + NEW.anonymised, + NEW.statistics_data); RETURN NEW; ELSE UPDATE wcs_all_forms SET @@ -948,7 +949,8 @@ BEGIN criticality_level = NEW.criticality_level - {criticality_levels}, geoloc_base_x = {geoloc_base_x}, geoloc_base_y = {geoloc_base_y}, - anonymised = NEW.anonymised + anonymised = NEW.anonymised, + statistics_data = NEW.statistics_data WHERE formdef_id = {formdef_id} AND id = OLD.id; RETURN NEW; END IF; @@ -1790,7 +1792,8 @@ def do_global_views(conn, cur): criticality_level integer, geoloc_base_x double precision, geoloc_base_y double precision, - anonymised timestamp with time zone + anonymised timestamp with time zone, + statistics_data jsonb , PRIMARY KEY(formdef_id, id) )""" ) @@ -1894,7 +1897,8 @@ def init_global_table(conn=None, cur=None): criticality_level - {criticality_levels}, {geoloc_base_x}, {geoloc_base_y}, - anonymised + anonymised, + statistics_data FROM {table_name} ON CONFLICT DO NOTHING; """.format( @@ -2502,6 +2506,7 @@ class SqlDataMixin(SqlMixin): ('digests', 'jsonb'), ('user_label', 'varchar'), ('auto_geoloc', 'point'), + ('statistics_data', 'jsonb'), ] def __init__(self, id=None): @@ -2598,7 +2603,8 @@ class SqlDataMixin(SqlMixin): '''UPDATE %s SET id_display = %%(id_display)s, digests = %%(digests)s, - user_label = %%(user_label)s + user_label = %%(user_label)s, + statistics_data = %%(statistics_data)s WHERE id = %%(id)s''' % self._table_name ) @@ -2609,6 +2615,7 @@ class SqlDataMixin(SqlMixin): 'id_display': self.id_display, 'digests': self.digests, 'user_label': self.user_label, + 'statistics_data': self.statistics_data, }, ) @@ -2630,6 +2637,7 @@ class SqlDataMixin(SqlMixin): 'submission_channel': self.submission_channel, 'criticality_level': self.criticality_level, 'workflow_merged_roles_dict': self.workflow_merged_roles_dict, + 'statistics_data': self.statistics_data or {}, } if self.last_update_time: sql_dict['last_update_time'] = datetime.datetime.fromtimestamp(time.mktime(self.last_update_time)) @@ -4936,7 +4944,7 @@ def get_period_total( # 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) -SQL_LEVEL = (79, 'add translatable column to TranslatableMessage table') +SQL_LEVEL = (80, 'add statistics data column to formdata') def migrate_global_views(conn, cur): @@ -5199,10 +5207,12 @@ def migrate(): continue for formdata in formdef.data_class().select_iterator(): formdata._set_auto_fields(cur) # build digests - if sql_level < 69: + if sql_level < 80: # 58: add workflow_merged_roles_dict as a jsonb column with # combined formdef and formdata value. # 69: add auto_geoloc field to form/card tables + # 80: add jsonb column to hold statistics data + for formdef in FormDef.select() + CardDef.select(): do_formdef_tables(formdef, rebuild_views=False, rebuild_global_views=False) migrate_views(conn, cur) -- 2.39.2 From 718723d44d961eb752da6acd1cc9d2e4912bdd04 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 24 Jan 2023 18:10:46 +0100 Subject: [PATCH 3/4] admin: update statistics data on field display location change (#73770) --- tests/admin_pages/test_block.py | 35 +++++++++++++++++++++++++++ tests/admin_pages/test_form.py | 27 +++++++++++++++++++++ tests/admin_pages/test_workflow.py | 38 ++++++++++++++++++++++++++++++ wcs/admin/blocks.py | 6 +++++ wcs/admin/fields.py | 11 ++++++++- wcs/admin/workflows.py | 10 +++++++- wcs/formdef.py | 4 ++++ 7 files changed, 129 insertions(+), 2 deletions(-) diff --git a/tests/admin_pages/test_block.py b/tests/admin_pages/test_block.py index b5e178d48..22c7f5922 100644 --- a/tests/admin_pages/test_block.py +++ b/tests/admin_pages/test_block.py @@ -501,3 +501,38 @@ def test_block_duplicate(pub): block_copy = BlockDef.get_by_slug('other_copy') assert len(block_copy.fields) == 2 + + +def test_block_field_statistics_data_update(pub): + create_superuser(pub) + + BlockDef.wipe() + block = BlockDef() + block.name = 'Foobar' + block.fields = [fields.BoolField(id='1', label='Bool', varname='bool', type='comment')] + block.store() + + FormDef.wipe() + formdef = FormDef() + formdef.name = 'form title' + formdef.fields = [ + fields.BlockField(id='0', label='test', type='block:%s' % block.slug), + ] + formdef.store() + + formdata = formdef.data_class()() + formdata.just_created() + formdata.data['0'] = {'data': [{'1': True}]} + formdata.store() + + assert not formdata.statistics_data + + app = login(get_app(pub)) + resp = app.get('/backoffice/forms/blocks/%s/1/' % block.id) + + resp.form['display_locations$element3'] = True + resp = resp.form.submit('submit').follow() + assert 'Statistics data will be collected in the background.' in resp.text + + formdata.refresh_from_storage() + assert formdata.statistics_data == {'bool': [True]} diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py index 77d6446f2..ca9a08d24 100644 --- a/tests/admin_pages/test_form.py +++ b/tests/admin_pages/test_form.py @@ -3765,3 +3765,30 @@ def test_form_import_fields(pub): ('5', 'field 4'), ('3', 'Page 2'), ] + + +def test_form_field_statistics_data_update(pub): + create_superuser(pub) + + FormDef.wipe() + formdef = FormDef() + formdef.name = 'form title' + formdef.fields = [fields.BoolField(id='1', label='Bool', varname='bool', type='comment')] + formdef.store() + + formdata = formdef.data_class()() + formdata.just_created() + formdata.data['1'] = True + formdata.store() + + assert not formdata.statistics_data + + app = login(get_app(pub)) + resp = app.get('/backoffice/forms/1/fields/1/') + + resp.form['display_locations$element3'] = True + resp = resp.form.submit('submit').follow() + assert 'Statistics data will be collected in the background.' in resp.text + + formdata.refresh_from_storage() + assert formdata.statistics_data == {'bool': [True]} diff --git a/tests/admin_pages/test_workflow.py b/tests/admin_pages/test_workflow.py index c63e8ec55..b55320a94 100644 --- a/tests/admin_pages/test_workflow.py +++ b/tests/admin_pages/test_workflow.py @@ -3745,3 +3745,41 @@ def test_remove_tracking_code_details(pub): workflow.store() resp = app.get('/backoffice/workflows/%s/status/%s/' % (workflow.id, baz_status.id)) assert 'Remove Tracking Code (replace with a new one)' in resp.text + + +def test_workflow_backoffice_field_statistics_data_update(pub): + create_superuser(pub) + + CardDef.wipe() + FormDef.wipe() + Workflow.wipe() + workflow = Workflow(name='foo') + workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow) + workflow.backoffice_fields_formdef.fields = [ + fields.BoolField(id='1', label='Bool', varname='bool', type='comment') + ] + workflow.store() + + FormDef.wipe() + formdef = FormDef() + formdef.name = 'form title' + formdef.workflow = workflow + formdef.store() + + app = login(get_app(pub)) + + formdata = formdef.data_class()() + formdata.data['1'] = True + formdata.store() + + assert not formdata.statistics_data + + app = login(get_app(pub)) + resp = app.get('/backoffice/workflows/1/backoffice-fields/fields/1/') + + resp.form['display_locations$element2'] = True + resp = resp.form.submit('submit').follow() + assert 'Statistics data will be collected in the background.' in resp.text + + formdata.refresh_from_storage() + assert formdata.statistics_data == {'bool': [True]} diff --git a/wcs/admin/blocks.py b/wcs/admin/blocks.py index 3707c8f23..056696f0a 100644 --- a/wcs/admin/blocks.py +++ b/wcs/admin/blocks.py @@ -24,6 +24,7 @@ from wcs.admin.fields import FieldDefPage, FieldsDirectory from wcs.backoffice.snapshots import SnapshotsDirectory from wcs.blocks import BlockDef, BlockdefImportError from wcs.categories import BlockCategory +from wcs.formdef import UpdateStatisticsDataAfterJob from wcs.qommon import _, misc, template from wcs.qommon.backoffice.menu import html_top from wcs.qommon.errors import AccessForbiddenError, TraversalError @@ -35,6 +36,11 @@ class BlockFieldDefPage(FieldDefPage): anchor = '#itemId_%s' % field.id if field else '' return redirect('../%s' % anchor) + def schedule_statistics_data_update(self): + get_response().add_after_job( + UpdateStatisticsDataAfterJob(formdefs=self.objectdef.get_usage_formdefs()) + ) + class BlockDirectory(FieldsDirectory): _q_exports = [ diff --git a/wcs/admin/fields.py b/wcs/admin/fields.py index 50a2080f7..df53c82e5 100644 --- a/wcs/admin/fields.py +++ b/wcs/admin/fields.py @@ -25,7 +25,7 @@ from wcs import fields from wcs.admin import utils from wcs.carddef import CardDef from wcs.fields import BlockField, get_field_options -from wcs.formdef import FormDef +from wcs.formdef import FormDef, UpdateStatisticsDataAfterJob from wcs.qommon import _, errors, get_cfg, misc from wcs.qommon.admin.menu import command_icon from wcs.qommon.backoffice.menu import html_top @@ -70,6 +70,7 @@ class FieldDefPage(Directory): def _q_index(self): form = self.form() redo = False + old_display_locations = self.field.display_locations.copy() if form.get_submit() == 'cancel': return redirect('../#itemId_%s' % self.field.id) @@ -111,6 +112,11 @@ class FieldDefPage(Directory): return r.getvalue() self.submit(form) + + if 'statistics' in self.field.display_locations and 'statistics' not in old_display_locations: + self.schedule_statistics_data_update() + get_session().message = ('info', _('Statistics data will be collected in the background.')) + if form.get_widget('items') is None and self.field.type == 'item': return redirect('.') @@ -128,6 +134,9 @@ class FieldDefPage(Directory): return redirect('../#itemId_%s' % self.field.id) + def schedule_statistics_data_update(self): + get_response().add_after_job(UpdateStatisticsDataAfterJob(formdefs=[self.objectdef])) + def submit(self, form): for f in self.field.get_admin_attributes(): widget = form.get_widget(f) diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py index 5b1519bff..f2483761a 100644 --- a/wcs/admin/workflows.py +++ b/wcs/admin/workflows.py @@ -32,7 +32,7 @@ from wcs.backoffice.snapshots import SnapshotsDirectory from wcs.carddef import CardDef from wcs.categories import WorkflowCategory from wcs.formdata import Evolution -from wcs.formdef import FormDef +from wcs.formdef import FormDef, UpdateStatisticsDataAfterJob from wcs.qommon import _, errors, force_str, misc, template from wcs.qommon.admin.menu import command_icon from wcs.qommon.backoffice.menu import html_top @@ -1083,6 +1083,14 @@ class WorkflowBackofficeFieldDefPage(FieldDefPage): display_locations.options = display_locations.options[1:] return form + def schedule_statistics_data_update(self): + formdefs = [ + x + for x in FormDef.select(lightweight=True) + CardDef.select(lightweight=True) + if x.workflow_id == self.objectdef.workflow.id + ] + get_response().add_after_job(UpdateStatisticsDataAfterJob(formdefs=formdefs)) + class WorkflowVariablesFieldsDirectory(FieldsDirectory): _q_exports = ['', 'update_order', 'new'] diff --git a/wcs/formdef.py b/wcs/formdef.py index b72cb8b71..f73381a57 100644 --- a/wcs/formdef.py +++ b/wcs/formdef.py @@ -2163,3 +2163,7 @@ class UpdateDigestAfterJob(AfterJob): formdef = formdef_class.get(formdef_id) for formdata in formdef.data_class().select(order_by='id'): formdata.store() + + +class UpdateStatisticsDataAfterJob(UpdateDigestAfterJob): + label = _('Updating statistics data') -- 2.39.2 From 271f87477947c82242b0fb1107714fcc78d0d252 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Wed, 18 Jan 2023 14:24:58 +0100 Subject: [PATCH 4/4] statistics: use new formdata column to compute counts (#73770) --- tests/api/test_statistics.py | 15 +++++++------ wcs/sql.py | 10 +++++++-- wcs/statistics/views.py | 41 +++++++++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py index ac641676c..f6e82e013 100644 --- a/tests/api/test_statistics.py +++ b/tests/api/test_statistics.py @@ -708,6 +708,7 @@ def test_statistics_forms_count_subfilters_query_same_varname(pub, formdef): ] formdef.store() + formdatas = [] for i in range(5): formdata = formdef.data_class()() formdata.just_created() @@ -718,6 +719,7 @@ def test_statistics_forms_count_subfilters_query_same_varname(pub, formdef): formdata.data['1'] = 'bar' formdata.data['2'] = 'foo' formdata.store() + formdatas.append(formdata) url = '/api/statistics/forms/count/?form=%s' % formdef.url_name resp = get_app(pub).get(sign_uri(url + '&filter-test=foo')) @@ -725,14 +727,15 @@ def test_statistics_forms_count_subfilters_query_same_varname(pub, formdef): formdef.fields[0].display_locations = ['statistics'] formdef.store() + for formdata in formdatas: + formdata.store() # refresh statistics_data column - # filter criterias is "f1 == 'foo' and f2 == 'foo'" hence one result, this should be improved + # first non empty value is used : 4 are 'foo' and one is 'bar' hence 4 results resp = get_app(pub).get(sign_uri(url + '&filter-test=foo')) - assert resp.json['data']['series'] == [{'data': [1], 'label': 'Forms Count'}] + assert resp.json['data']['series'] == [{'data': [4], 'label': 'Forms Count'}] - # filter criterias is "f1 == 'bar' and f2 == 'bar'" hence no results, this should be improved resp = get_app(pub).get(sign_uri(url + '&filter-test=bar')) - assert resp.json['data']['series'] == [{'data': [], 'label': 'Forms Count'}] + assert resp.json['data']['series'] == [{'data': [1], 'label': 'Forms Count'}] def test_statistics_forms_count_subfilters_query_integer_items(pub, formdef): @@ -943,6 +946,7 @@ def test_statistics_forms_count_group_by_same_varname(pub, formdef): formdef.fields[0].display_locations = ['statistics'] formdef.store() + formdata.store() # refresh statistics_data column # group by uses first field marked for statistics resp = get_app(pub).get(sign_uri(url + '&group-by=test')) @@ -954,9 +958,8 @@ def test_statistics_forms_count_group_by_same_varname(pub, formdef): formdata.data['2'] = 'foo' formdata.store() - # second field is ignored resp = get_app(pub).get(sign_uri(url + '&group-by=test')) - assert resp.json['data']['series'] == [{'data': [1], 'label': 'foo'}, {'data': [1], 'label': 'None'}] + assert resp.json['data']['series'] == [{'data': [2], 'label': 'foo'}] def test_statistics_cards_count(pub): diff --git a/wcs/sql.py b/wcs/sql.py index f7024ba04..64d336aa5 100644 --- a/wcs/sql.py +++ b/wcs/sql.py @@ -181,7 +181,9 @@ def pickle_loads(value): class Criteria(qommon.storage.Criteria): def __init__(self, attribute, value, **kwargs): - self.attribute = attribute.replace('-', '_') + self.attribute = attribute + if '->' not in attribute: + self.attribute = self.attribute.replace('-', '_') self.value = value self.field = kwargs.get('field') @@ -4647,7 +4649,11 @@ def get_period_query( formdef_class = criteria.value continue - if criteria.__class__.__name__ == 'Equal' and criteria.attribute == 'formdef_id': + if ( + formdef_class + and 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 table_name = get_formdef_table_name(formdef_class.get(criteria.value)) diff --git a/wcs/statistics/views.py b/wcs/statistics/views.py index 04e00a361..abf1a7967 100644 --- a/wcs/statistics/views.py +++ b/wcs/statistics/views.py @@ -30,7 +30,7 @@ from wcs.categories import Category from wcs.formdata import FormData from wcs.formdef import FormDef from wcs.qommon import _, misc, pgettext_lazy -from wcs.qommon.storage import Contains, Equal, GreaterOrEqual, Less, NotNull, Null, Or, StrictNotEqual +from wcs.qommon.storage import Contains, Equal, GreaterOrEqual, Less, Null, Or, StrictNotEqual class RestrictedView(View): @@ -252,11 +252,7 @@ class FormsCountView(RestrictedView): except KeyError: return HttpResponseBadRequest('invalid form') 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)) + self.set_formdef_parameters(totals_kwargs, formdef) totals_kwargs['criterias'].extend(self.get_filters_criterias(formdef, form_page)) self.set_group_by_parameters(group_by, formdef, form_page, totals_kwargs, group_labels) subfilters = self.get_subfilters(form_page, group_by) @@ -314,8 +310,27 @@ class FormsCountView(RestrictedView): {'data': {'x_labels': x_labels, 'series': series, 'subfilters': subfilters}, 'err': 0} ) + def set_formdef_parameters(self, totals_kwargs, formdef): + # set formdef_klass to None to deactivate switching to formdef specific table + totals_kwargs['criterias'].append(Equal('formdef_klass', None)) + totals_kwargs['criterias'].append(Equal('formdef_id', formdef.id)) + + def transform_criteria(self, criteria): + if not hasattr(criteria, 'field'): + return criteria + + attribute = "statistics_data->'%s'" % criteria.field.varname + + if isinstance(criteria.value, bool): + value = str(criteria.value).lower() + else: + value = '"%s"' % criteria.value + + return sql.ArrayContains(attribute, value) + def get_filters_criterias(self, formdef, form_page): criterias = form_page.get_criterias_from_query(statistics_fields_only=True) + criterias = [self.transform_criteria(criteria) for criteria in criterias] selected_status = self.request.GET.get('filter-status') applied_filters = None @@ -471,10 +486,10 @@ class FormsCountView(RestrictedView): if group_by_field.type == 'status': totals_kwargs['group_by'] = 'status' else: - totals_kwargs['group_by'] = sql.get_field_id(group_by_field) + totals_kwargs['group_by'] = "statistics_data->'%s'" % group_by_field.varname if self.request.GET.get('hide_none_label') == 'true': - totals_kwargs['criterias'].append(NotNull(totals_kwargs['group_by'])) + totals_kwargs['criterias'].append(StrictNotEqual(totals_kwargs['group_by'], '[]')) group_labels.update(self.get_group_labels(group_by_field, formdef, form_page, group_by)) @@ -494,6 +509,8 @@ class FormsCountView(RestrictedView): groups = total[1] if not isinstance(groups, list): groups = [groups] + if not groups: + groups = [None] for group in groups: totals_by_group[group] += total[2] seen_group_values.add(group) @@ -516,6 +533,8 @@ class FormsCountView(RestrictedView): for groups, total in totals: if not isinstance(groups, list): groups = [groups] + if not groups: + groups = [None] for group in groups: totals_by_group[group] += total @@ -562,6 +581,12 @@ class CardsCountView(FormsCountView): has_global_count_support = False label = _('Cards Count') + def set_formdef_parameters(self, totals_kwargs, formdef): + # 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', CardDef)) + totals_kwargs['criterias'].append(Equal('formdef_id', formdef.id)) + class ResolutionTimeView(RestrictedView): formdef_class = FormDef -- 2.39.2