statistiques, ajouter une colonne commune à wcs_all_form avec les données nécessaires (#73770) #63

Merged
vdeniaud merged 4 commits from wip/73770-statistiques-ajouter-une-colonne into main 2023-02-14 18:16:16 +01:00
11 changed files with 265 additions and 29 deletions

View File

@ -501,3 +501,38 @@ def test_block_duplicate(pub):
block_copy = BlockDef.get_by_slug('other_copy') block_copy = BlockDef.get_by_slug('other_copy')
assert len(block_copy.fields) == 2 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]}

View File

@ -3765,3 +3765,30 @@ def test_form_import_fields(pub):
('5', 'field 4'), ('5', 'field 4'),
('3', 'Page 2'), ('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]}

View File

@ -3745,3 +3745,41 @@ def test_remove_tracking_code_details(pub):
workflow.store() workflow.store()
resp = app.get('/backoffice/workflows/%s/status/%s/' % (workflow.id, baz_status.id)) 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 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]}

View File

@ -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)) new_resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?form=%s' % formdef.url_name))
assert new_resp.json == resp.json 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 # remove fields and statuses
workflow = Workflow(name='Empty wf') workflow = Workflow(name='Empty wf')
workflow.store() workflow.store()
@ -587,7 +604,7 @@ def test_statistics_forms_count_subfilters_query(pub, formdef):
formdata.data['1'] = True formdata.data['1'] = True
formdata.data['2'] = 'foo' formdata.data['2'] = 'foo'
formdata.data['3'] = ['bar', 'baz'] formdata.data['3'] = ['bar', 'baz']
formdata.data['4'] = {'data': [{'1': True}]} formdata.data['4'] = {'data': [{'1': True}, {'1': False}]}
elif i % 2: elif i % 2:
formdata.data['1'] = False formdata.data['1'] = False
formdata.data['2'] = 'baz' 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 assert resp.json['data']['series'][0]['data'][0] == 13
resp = get_app(pub).get(sign_uri(url + '&filter-blockdata_bool=false')) 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 # filter on status
resp = get_app(pub).get(sign_uri(url + '&filter-status=_all')) resp = get_app(pub).get(sign_uri(url + '&filter-status=_all'))
@ -691,6 +708,7 @@ def test_statistics_forms_count_subfilters_query_same_varname(pub, formdef):
] ]
formdef.store() formdef.store()
formdatas = []
for i in range(5): for i in range(5):
formdata = formdef.data_class()() formdata = formdef.data_class()()
formdata.just_created() formdata.just_created()
@ -701,6 +719,7 @@ def test_statistics_forms_count_subfilters_query_same_varname(pub, formdef):
formdata.data['1'] = 'bar' formdata.data['1'] = 'bar'
formdata.data['2'] = 'foo' formdata.data['2'] = 'foo'
formdata.store() formdata.store()
formdatas.append(formdata)
url = '/api/statistics/forms/count/?form=%s' % formdef.url_name url = '/api/statistics/forms/count/?form=%s' % formdef.url_name
resp = get_app(pub).get(sign_uri(url + '&filter-test=foo')) resp = get_app(pub).get(sign_uri(url + '&filter-test=foo'))
@ -708,14 +727,34 @@ def test_statistics_forms_count_subfilters_query_same_varname(pub, formdef):
formdef.fields[0].display_locations = ['statistics'] formdef.fields[0].display_locations = ['statistics']
formdef.store() 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')) resp = get_app(pub).get(sign_uri(url + '&filter-test=foo'))
assert resp.json['data']['series'] == [{'data': [4], 'label': 'Forms Count'}]
resp = get_app(pub).get(sign_uri(url + '&filter-test=bar'))
assert resp.json['data']['series'] == [{'data': [1], 'label': 'Forms Count'}] assert resp.json['data']['series'] == [{'data': [1], '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')) def test_statistics_forms_count_subfilters_query_integer_items(pub, formdef):
assert resp.json['data']['series'] == [{'data': [], 'label': 'Forms Count'}] 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]) @pytest.mark.parametrize('anonymise', [False, True])
@ -907,6 +946,7 @@ def test_statistics_forms_count_group_by_same_varname(pub, formdef):
formdef.fields[0].display_locations = ['statistics'] formdef.fields[0].display_locations = ['statistics']
formdef.store() formdef.store()
formdata.store() # refresh statistics_data column
# group by uses first field marked for statistics # group by uses first field marked for statistics
resp = get_app(pub).get(sign_uri(url + '&group-by=test')) resp = get_app(pub).get(sign_uri(url + '&group-by=test'))
@ -918,9 +958,8 @@ def test_statistics_forms_count_group_by_same_varname(pub, formdef):
formdata.data['2'] = 'foo' formdata.data['2'] = 'foo'
formdata.store() formdata.store()
# second field is ignored
resp = get_app(pub).get(sign_uri(url + '&group-by=test')) 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): def test_statistics_cards_count(pub):

View File

@ -24,6 +24,7 @@ from wcs.admin.fields import FieldDefPage, FieldsDirectory
from wcs.backoffice.snapshots import SnapshotsDirectory from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.blocks import BlockDef, BlockdefImportError from wcs.blocks import BlockDef, BlockdefImportError
from wcs.categories import BlockCategory from wcs.categories import BlockCategory
from wcs.formdef import UpdateStatisticsDataAfterJob
from wcs.qommon import _, misc, template from wcs.qommon import _, misc, template
from wcs.qommon.backoffice.menu import html_top from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.errors import AccessForbiddenError, TraversalError from wcs.qommon.errors import AccessForbiddenError, TraversalError
@ -35,6 +36,11 @@ class BlockFieldDefPage(FieldDefPage):
anchor = '#itemId_%s' % field.id if field else '' anchor = '#itemId_%s' % field.id if field else ''
return redirect('../%s' % anchor) return redirect('../%s' % anchor)
def schedule_statistics_data_update(self):
get_response().add_after_job(
UpdateStatisticsDataAfterJob(formdefs=self.objectdef.get_usage_formdefs())

Plutôt que tel quel le UpdateDigestAfterJob je préfererais un nouveau job (très simple, ça peut juste être une classe qui hérite de UpdateDigestAfterJob et lui met un attribut label adapté aux statistiques).

Plutôt que tel quel le UpdateDigestAfterJob je préfererais un nouveau job (très simple, ça peut juste être une classe qui hérite de UpdateDigestAfterJob et lui met un attribut label adapté aux statistiques).

Ajouté « UpdateStatisticsDataAfterJob ».

Ajouté « UpdateStatisticsDataAfterJob ».
)
class BlockDirectory(FieldsDirectory): class BlockDirectory(FieldsDirectory):
_q_exports = [ _q_exports = [

View File

@ -25,7 +25,7 @@ from wcs import fields
from wcs.admin import utils from wcs.admin import utils
from wcs.carddef import CardDef from wcs.carddef import CardDef
from wcs.fields import BlockField, get_field_options 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 import _, errors, get_cfg, misc
from wcs.qommon.admin.menu import command_icon from wcs.qommon.admin.menu import command_icon
from wcs.qommon.backoffice.menu import html_top from wcs.qommon.backoffice.menu import html_top
@ -70,6 +70,7 @@ class FieldDefPage(Directory):
def _q_index(self): def _q_index(self):
form = self.form() form = self.form()
redo = False redo = False
old_display_locations = self.field.display_locations.copy()
if form.get_submit() == 'cancel': if form.get_submit() == 'cancel':
return redirect('../#itemId_%s' % self.field.id) return redirect('../#itemId_%s' % self.field.id)
@ -111,6 +112,11 @@ class FieldDefPage(Directory):
return r.getvalue() return r.getvalue()
self.submit(form) 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': if form.get_widget('items') is None and self.field.type == 'item':
return redirect('.') return redirect('.')
@ -128,6 +134,9 @@ class FieldDefPage(Directory):
return redirect('../#itemId_%s' % self.field.id) 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): def submit(self, form):
for f in self.field.get_admin_attributes(): for f in self.field.get_admin_attributes():
widget = form.get_widget(f) widget = form.get_widget(f)

View File

@ -32,7 +32,7 @@ from wcs.backoffice.snapshots import SnapshotsDirectory
from wcs.carddef import CardDef from wcs.carddef import CardDef
from wcs.categories import WorkflowCategory from wcs.categories import WorkflowCategory
from wcs.formdata import Evolution 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 import _, errors, force_str, misc, template
from wcs.qommon.admin.menu import command_icon from wcs.qommon.admin.menu import command_icon
from wcs.qommon.backoffice.menu import html_top from wcs.qommon.backoffice.menu import html_top
@ -1083,6 +1083,14 @@ class WorkflowBackofficeFieldDefPage(FieldDefPage):
display_locations.options = display_locations.options[1:] display_locations.options = display_locations.options[1:]
return form 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): class WorkflowVariablesFieldsDirectory(FieldsDirectory):
_q_exports = ['', 'update_order', 'new'] _q_exports = ['', 'update_order', 'new']

View File

@ -302,6 +302,7 @@ class FormData(StorableObject):
workflow_data = None workflow_data = None
workflow_roles = None workflow_roles = None
geolocations = None geolocations = None
statistics_data = None
_formdef = None _formdef = None
@ -521,8 +522,9 @@ class FormData(StorableObject):
changed = False changed = False
def get_all_fields(): def get_all_fields(with_backoffice_fields=False):
for field in self.formdef.fields: fields = self.formdef.get_all_fields() if with_backoffice_fields else self.formdef.fields
for field in fields:
yield field yield field
if field.key == 'block': if field.key == 'block':
try: try:
@ -608,6 +610,33 @@ class FormData(StorableObject):
if digests: if digests:
self.digests = digests self.digests = digests
changed = True 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 return changed
def get_lateral_block(self): def get_lateral_block(self):

View File

@ -2163,3 +2163,7 @@ class UpdateDigestAfterJob(AfterJob):
formdef = formdef_class.get(formdef_id) formdef = formdef_class.get(formdef_id)
for formdata in formdef.data_class().select(order_by='id'): for formdata in formdef.data_class().select(order_by='id'):
formdata.store() formdata.store()
class UpdateStatisticsDataAfterJob(UpdateDigestAfterJob):
label = _('Updating statistics data')

View File

@ -181,7 +181,9 @@ def pickle_loads(value):
class Criteria(qommon.storage.Criteria): class Criteria(qommon.storage.Criteria):
def __init__(self, attribute, value, **kwargs): 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.value = value
self.field = kwargs.get('field') self.field = kwargs.get('field')
@ -925,7 +927,8 @@ BEGIN
NEW.criticality_level - {criticality_levels}, NEW.criticality_level - {criticality_levels},
{geoloc_base_x}, {geoloc_base_x},
{geoloc_base_y}, {geoloc_base_y},
NEW.anonymised); NEW.anonymised,
NEW.statistics_data);
RETURN NEW; RETURN NEW;
ELSE ELSE
UPDATE wcs_all_forms SET UPDATE wcs_all_forms SET
@ -948,7 +951,8 @@ BEGIN
criticality_level = NEW.criticality_level - {criticality_levels}, criticality_level = NEW.criticality_level - {criticality_levels},
geoloc_base_x = {geoloc_base_x}, geoloc_base_x = {geoloc_base_x},
geoloc_base_y = {geoloc_base_y}, 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; WHERE formdef_id = {formdef_id} AND id = OLD.id;
RETURN NEW; RETURN NEW;
END IF; END IF;
@ -1790,7 +1794,8 @@ def do_global_views(conn, cur):
criticality_level integer, criticality_level integer,
geoloc_base_x double precision, geoloc_base_x double precision,
geoloc_base_y 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) , PRIMARY KEY(formdef_id, id)
)""" )"""
) )
@ -1894,7 +1899,8 @@ def init_global_table(conn=None, cur=None):
criticality_level - {criticality_levels}, criticality_level - {criticality_levels},
{geoloc_base_x}, {geoloc_base_x},
{geoloc_base_y}, {geoloc_base_y},
anonymised anonymised,
statistics_data
FROM {table_name} FROM {table_name}
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
""".format( """.format(
@ -2502,6 +2508,7 @@ class SqlDataMixin(SqlMixin):
('digests', 'jsonb'), ('digests', 'jsonb'),
('user_label', 'varchar'), ('user_label', 'varchar'),
('auto_geoloc', 'point'), ('auto_geoloc', 'point'),
('statistics_data', 'jsonb'),
] ]
def __init__(self, id=None): def __init__(self, id=None):
@ -2598,7 +2605,8 @@ class SqlDataMixin(SqlMixin):
'''UPDATE %s '''UPDATE %s
SET id_display = %%(id_display)s, SET id_display = %%(id_display)s,
digests = %%(digests)s, digests = %%(digests)s,
user_label = %%(user_label)s user_label = %%(user_label)s,
statistics_data = %%(statistics_data)s
WHERE id = %%(id)s''' WHERE id = %%(id)s'''
% self._table_name % self._table_name
) )
@ -2609,6 +2617,7 @@ class SqlDataMixin(SqlMixin):
'id_display': self.id_display, 'id_display': self.id_display,
'digests': self.digests, 'digests': self.digests,
'user_label': self.user_label, 'user_label': self.user_label,
'statistics_data': self.statistics_data,
}, },
) )
@ -2630,6 +2639,7 @@ class SqlDataMixin(SqlMixin):
'submission_channel': self.submission_channel, 'submission_channel': self.submission_channel,
'criticality_level': self.criticality_level, 'criticality_level': self.criticality_level,
'workflow_merged_roles_dict': self.workflow_merged_roles_dict, 'workflow_merged_roles_dict': self.workflow_merged_roles_dict,
'statistics_data': self.statistics_data or {},
} }
if self.last_update_time: if self.last_update_time:
sql_dict['last_update_time'] = datetime.datetime.fromtimestamp(time.mktime(self.last_update_time)) sql_dict['last_update_time'] = datetime.datetime.fromtimestamp(time.mktime(self.last_update_time))
@ -4639,7 +4649,11 @@ def get_period_query(
formdef_class = criteria.value formdef_class = criteria.value
continue 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 # if there's a formdef_id specified, switch to using the
# specific table so we have access to all fields # specific table so we have access to all fields
table_name = get_formdef_table_name(formdef_class.get(criteria.value)) table_name = get_formdef_table_name(formdef_class.get(criteria.value))
@ -4936,7 +4950,7 @@ def get_period_total(
# latest migration, number + description (description is not used # latest migration, number + description (description is not used
# programmaticaly but will make sure git conflicts if two migrations are # programmaticaly but will make sure git conflicts if two migrations are
# separately added with the same number) # 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): def migrate_global_views(conn, cur):
@ -5199,10 +5213,12 @@ def migrate():
continue continue
for formdata in formdef.data_class().select_iterator(): for formdata in formdef.data_class().select_iterator():
formdata._set_auto_fields(cur) # build digests 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 # 58: add workflow_merged_roles_dict as a jsonb column with
# combined formdef and formdata value. # combined formdef and formdata value.
# 69: add auto_geoloc field to form/card tables # 69: add auto_geoloc field to form/card tables
# 80: add jsonb column to hold statistics data
for formdef in FormDef.select() + CardDef.select(): for formdef in FormDef.select() + CardDef.select():
do_formdef_tables(formdef, rebuild_views=False, rebuild_global_views=False) do_formdef_tables(formdef, rebuild_views=False, rebuild_global_views=False)
migrate_views(conn, cur) migrate_views(conn, cur)

View File

@ -30,7 +30,7 @@ from wcs.categories import Category
from wcs.formdata import FormData from wcs.formdata import FormData
from wcs.formdef import FormDef from wcs.formdef import FormDef
from wcs.qommon import _, misc, pgettext_lazy 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): class RestrictedView(View):
@ -252,11 +252,7 @@ class FormsCountView(RestrictedView):
except KeyError: except KeyError:
return HttpResponseBadRequest('invalid form') return HttpResponseBadRequest('invalid form')
form_page = self.formpage_class(formdef=formdef, update_breadcrumbs=False) form_page = self.formpage_class(formdef=formdef, update_breadcrumbs=False)
self.set_formdef_parameters(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', self.formdef_class))
totals_kwargs['criterias'].append(Equal('formdef_id', formdef.id))
totals_kwargs['criterias'].extend(self.get_filters_criterias(formdef, form_page)) 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) self.set_group_by_parameters(group_by, formdef, form_page, totals_kwargs, group_labels)
subfilters = self.get_subfilters(form_page, group_by) 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} {'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): def get_filters_criterias(self, formdef, form_page):
criterias = form_page.get_criterias_from_query(statistics_fields_only=True) 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') selected_status = self.request.GET.get('filter-status')
applied_filters = None applied_filters = None
@ -471,10 +486,10 @@ class FormsCountView(RestrictedView):
if group_by_field.type == 'status': if group_by_field.type == 'status':
totals_kwargs['group_by'] = 'status' totals_kwargs['group_by'] = 'status'
else: 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': 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)) 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] groups = total[1]
if not isinstance(groups, list): if not isinstance(groups, list):
groups = [groups] groups = [groups]
if not groups:
groups = [None]
for group in groups: for group in groups:
totals_by_group[group] += total[2] totals_by_group[group] += total[2]
seen_group_values.add(group) seen_group_values.add(group)
@ -516,6 +533,8 @@ class FormsCountView(RestrictedView):
for groups, total in totals: for groups, total in totals:
if not isinstance(groups, list): if not isinstance(groups, list):
groups = [groups] groups = [groups]
if not groups:
groups = [None]
for group in groups: for group in groups:
totals_by_group[group] += total totals_by_group[group] += total
@ -562,6 +581,12 @@ class CardsCountView(FormsCountView):
has_global_count_support = False has_global_count_support = False
label = _('Cards Count') 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): class ResolutionTimeView(RestrictedView):
formdef_class = FormDef formdef_class = FormDef