statistics: allow filtering by items field inside block (#75573) #205

Merged
vdeniaud merged 1 commits from wip/75573-statistiques-erreur-dans-set-aut into main 2023-04-20 09:47:40 +02:00
4 changed files with 47 additions and 29 deletions

View File

@ -74,7 +74,16 @@ def formdef(pub):
block = BlockDef()
block.name = 'foobar'
block.fields = [
fields.BoolField(id='1', label='Bool', type='bool', varname='bool', display_locations=['statistics'])
fields.BoolField(id='1', label='Bool', type='bool', varname='bool', display_locations=['statistics']),
fields.ItemsField(
id='2',
varname='block-items',
label='Block items',
type='items',
items=['foo', 'bar', 'baz'],
anonymise=False,
display_locations=['statistics'],
),
]
block.store()
@ -415,6 +424,7 @@ def test_statistics_forms_count_subfilters(pub, formdef):
formdata.data['2_display'] = 'Foo' if i % 2 else 'Baz'
formdata.data['3'] = ['foo'] if i % 2 else ['bar', 'baz']
formdata.data['3_display'] = 'Foo' if i % 2 else 'Bar, Baz'
formdata.data['4'] = {'data': [{'2': ['foo', 'bar'], '2_display': 'Foo, Bar'}]}
formdata.just_created()
formdata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple()
formdata.store()
@ -464,8 +474,16 @@ def test_statistics_forms_count_subfilters(pub, formdef):
'required': False,
}
# check boolean backoffice field subfilter
# check block items field subfilter
assert resp.json['data']['subfilters'][4] == {
'id': 'filter-blockdata_block-items',
'label': 'Block items',
'options': [{'id': 'bar', 'label': 'Bar'}, {'id': 'foo', 'label': 'Foo'}],
'required': False,
}
# check boolean backoffice field subfilter
assert resp.json['data']['subfilters'][5] == {
'id': 'filter-checkbox',
'label': 'Checkbox',
'options': [{'id': 'true', 'label': 'Yes'}, {'id': 'false', 'label': 'No'}],
@ -537,23 +555,6 @@ 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()
@ -578,12 +579,17 @@ 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}, {'1': False}]}
formdata.data['4'] = {
'data': [
{'1': True, '2': ['baz'], '2_display': 'Baz'},
{'1': False, '2': ['foo'], '2_display': 'Foo'},
]
}
elif i % 2:
formdata.data['1'] = False
formdata.data['2'] = 'baz'
formdata.data['3'] = ['baz']
formdata.data['4'] = {'data': [{'1': False}]}
formdata.data['4'] = {'data': [{'1': False, '2': ['foo', 'bar'], '2_display': 'Foo, Bar'}]}
formdata.jump_status('2')
formdata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple()
formdata.store()
@ -639,6 +645,16 @@ def test_statistics_forms_count_subfilters_query(pub, formdef):
resp = get_app(pub).get(sign_uri(url + '&filter-blockdata_bool=false'))
assert resp.json['data']['series'][0]['data'][0] == 16
# filter on block items field
resp = get_app(pub).get(sign_uri(url + '&filter-blockdata_block-items=foo'))
assert resp.json['data']['series'][0]['data'][0] == 16
resp = get_app(pub).get(sign_uri(url + '&filter-blockdata_block-items=bar'))
assert resp.json['data']['series'][0]['data'][0] == 3
resp = get_app(pub).get(sign_uri(url + '&filter-blockdata_block-items=baz'))
assert resp.json['data']['series'][0]['data'][0] == 13
# filter on status
resp = get_app(pub).get(sign_uri(url + '&filter-status=_all'))
assert resp.json['data']['series'][0]['data'][0] == 20

View File

@ -994,7 +994,7 @@ class FormPage(FormdefDirectoryBase):
# values used in repeated blocks, ex:
# {"data": [{"FOOBAR": "value1"}, {"FOOBAR": "value2}]}
# → ["value1", "value2"}
field1 = "jsonb_array_elements(%s->'data')->> '%s'" % (
field1 = "jsonb_array_elements(%s->'data')-> '%s'" % (
Review

Ce bout me reste mystérieux et à chaque fois j'abandonne la relecture quand j'arrive ici :/

(mais Pierre m'a dit que c'était ok donc je vais valider)

Ce bout me reste mystérieux et à chaque fois j'abandonne la relecture quand j'arrive ici :/ (mais Pierre m'a dit que c'était ok donc je vais valider)
Review

Ma compréhension c'est que ->> sort toujours l'élément du JSON sous forme de string, or la valeur d'un ItemsField c'est une liste, donc ça sort n'imp genre un truc qui vaut "['a', 'b']", enlever un chevron donne bien ['a', 'b'] une vraie liste.

Là où ça pourrait poser problème c'est si il y avait genre des entiers dans le JSON, et que derrière du code qui attendait des string reçoit maintenant des entiers. Mais je crois qu'il y a suffisamment de cast python str() explicites partout que ça a peu de chance d'arriver (confiance aux tests, aussi).

Ma compréhension c'est que `->>` sort toujours l'élément du JSON sous forme de string, or la valeur d'un ItemsField c'est une liste, donc ça sort n'imp genre un truc qui vaut `"['a', 'b']"`, enlever un chevron donne bien `['a', 'b']` une vraie liste. Là où ça pourrait poser problème c'est si il y avait genre des entiers dans le JSON, et que derrière du code qui attendait des string reçoit maintenant des entiers. Mais je crois qu'il y a suffisamment de cast python str() explicites partout que ça a peu de chance d'arriver (confiance aux tests, aussi).
sql.get_field_id(filter_field.block_field),
filter_field.id,
)
@ -1630,7 +1630,7 @@ class FormPage(FormdefDirectoryBase):
self.view.remove_self()
return redirect('..')
def get_formdef_fields(self):
def get_formdef_fields(self, include_block_items_fields=False):
yield FakeField('id', 'id', _('Number'))
if get_publisher().get_site_option('welco_url', 'variables'):
yield FakeField('submission_channel', 'submission_channel', _('Channel'))
@ -1651,7 +1651,7 @@ class FormPage(FormdefDirectoryBase):
for field in self.formdef.iter_fields(include_block_fields=True):
if getattr(field, 'block_field', None):
if field.key == 'items':
if field.key == 'items' and not include_block_items_fields:
# not yet
continue
yield field
@ -1828,7 +1828,7 @@ class FormPage(FormdefDirectoryBase):
get_session().message = ('warning', error_message)
criterias.append(Nothing())
for filter_field in fake_fields + list(self.get_formdef_fields()):
for filter_field in fake_fields + list(self.get_formdef_fields(include_block_items_fields=True)):
if filter_field.type not in self.get_filterable_field_types():
continue

View File

@ -621,8 +621,10 @@ class FormData(StorableObject):
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 = data.get(field.id)
if not isinstance(values, list):
values = [values]
items.update(values)
values = list(items)
else:
values = self.data.get(field.id)

View File

@ -380,7 +380,7 @@ class FormsCountView(RestrictedView):
def get_form_subfilters(self, form_page, group_by):
subfilters = []
field_choices = []
for field in form_page.get_formdef_fields():
for field in form_page.get_formdef_fields(include_block_items_fields=True):
if not getattr(field, 'include_in_statistics', False) or not field.contextual_varname:
continue
@ -458,7 +458,7 @@ class FormsCountView(RestrictedView):
def get_group_by_field(self, form_page, group_by):
fields = [
x
for x in form_page.get_formdef_fields()
for x in form_page.get_formdef_fields(include_block_items_fields=True)
if getattr(x, 'contextual_varname', None) == group_by
and getattr(x, 'include_in_statistics', False)
]