dataviz: add support for subfilters (#61083)

This commit is contained in:
Valentin Deniaud 2022-01-24 18:13:24 +01:00
parent e42495fae1
commit 30d1483f03
5 changed files with 173 additions and 11 deletions

View File

@ -65,7 +65,7 @@ class ChartFiltersMixin:
def get_filter_fields(self, cell):
fields = OrderedDict()
for filter_ in cell.statistic.filters:
for filter_ in cell.available_filters:
filter_id = filter_['id']
choices = [(option['id'], option['label']) for option in filter_['options']]
initial = cell.filter_params.get(filter_id, filter_.get('default'))
@ -84,6 +84,7 @@ class ChartFiltersMixin:
fields[filter_id] = field_class(
label=filter_['label'], choices=choices, required=required, initial=initial
)
fields[filter_id].is_filter_field = True
# extend time interval choices if possible
if 'time_interval' in fields:
@ -139,26 +140,42 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm):
stat_field.queryset = stat_field.queryset.filter(
Q(available=True) | Q(pk=self.instance.statistic.pk)
)
self.add_filter_fields()
new_fields = OrderedDict()
for field_name, field in self.fields.items():
new_fields[field_name] = field
if field_name == 'statistic':
# insert filter fields after statistic field
new_fields.update(self.get_filter_fields(self.instance))
self.fields = new_fields
def add_filter_fields(self):
new_fields = OrderedDict()
for field_name, field in self.fields.items():
new_fields[field_name] = field
if field_name == 'statistic':
# insert filter fields after statistic field
new_fields.update(self.get_filter_fields(self.instance))
self.fields = new_fields
def save(self, *args, **kwargs):
if 'statistic' in self.changed_data:
self.instance.filter_params.clear()
self.instance.time_range = ''
for filter_ in self.instance.statistic.filters:
for filter_ in self.instance.available_filters:
if 'default' in filter_:
self.instance.filter_params[filter_['id']] = filter_['default']
else:
for filter_ in self.instance.statistic.filters:
for filter_ in self.instance.available_filters:
self.instance.filter_params[filter_['id']] = self.cleaned_data.get(filter_['id'])
return super().save(*args, **kwargs)
cell = super().save(*args, **kwargs)
for filter_ in cell.available_filters:
if filter_.get('has_subfilters') and filter_['id'] in self.changed_data:
cell.update_subfilters()
self.fields = OrderedDict(
(name, field)
for name, field in self.fields.items()
if not hasattr(field, 'is_filter_field')
)
self.add_filter_fields()
break
return cell
def clean(self):
for template_field in ('time_range_start_template', 'time_range_end_template'):

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.19 on 2022-01-25 16:21
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dataviz', '0021_chartfilterscell'),
]
operations = [
migrations.AddField(
model_name='chartngcell',
name='subfilters',
field=django.contrib.postgres.fields.jsonb.JSONField(default=list),
),
]

View File

@ -186,6 +186,7 @@ class ChartNgCell(CellBase):
'This list may take a few seconds to be updated, please refresh the page if an item is missing.'
),
)
subfilters = JSONField(default=list)
filter_params = JSONField(default=dict)
title = models.CharField(_('Title'), max_length=150, blank=True)
time_range = models.CharField(
@ -679,6 +680,25 @@ class ChartNgCell(CellBase):
for i, serie in enumerate(data['series']):
serie['data'] = [values[i] for values in aggregates.values()]
@property
def available_filters(self):
return self.statistic.filters + self.subfilters
def update_subfilters(self):
response = self.get_statistic_data()
try:
response.raise_for_status()
data = response.json()['data']
except Exception:
return
new_subfilters = data.get('subfilters', [])
if self.subfilters != new_subfilters:
self.subfilters = new_subfilters
subfilter_ids = {filter_['id'] for filter_ in self.available_filters}
self.filter_params = {k: v for k, v in self.filter_params.items() if k in subfilter_ids}
self.save()
@register_cell_class
class ChartFiltersCell(CellBase):

View File

@ -102,3 +102,4 @@ def refresh_statistics_data(cell_pk):
except ChartNgCell.DoesNotExist:
return
cell.get_statistic_data(invalidate_cache=True)
cell.update_subfilters()

View File

@ -407,6 +407,31 @@ STATISTICS_LIST = {
}
],
},
{
'url': 'https://authentic.example.com/api/statistics/with-subfilter/',
'name': 'With subfilter',
'id': 'with-subfilter',
'filters': [
{
'id': 'form',
'label': 'Form',
'has_subfilters': True,
'options': [
{'id': 'food-request', 'label': 'Food request'},
{'id': 'contact', 'label': 'Contact'},
{'id': 'error', 'label': 'Error'},
],
},
{
'id': 'other',
'label': 'Other',
'options': [
{'id': 'one', 'label': 'One'},
{'id': 'two', 'label': 'two'},
],
},
],
},
]
}
@ -473,6 +498,28 @@ def new_api_mock(url, request):
},
}
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
if url.path == '/api/statistics/with-subfilter/':
response = {
'data': {
'series': [{'data': [None, 16, 2], 'label': 'Serie 1'}],
'x_labels': ['2020-10', '2020-11', '2020-12'],
'subfilters': [],
},
}
if 'form=food-request' in url.query:
response['data']['subfilters'] = [
{
"id": "menu",
"label": "Menu",
"options": [
{"id": "meat", "label": "Meat"},
{"id": "vegan", "label": "Vegan"},
],
}
]
if 'form=error' in url.query:
return {'content': b'', 'request': request, 'status_code': 404}
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
@pytest.fixture
@ -1320,6 +1367,64 @@ def test_chartng_cell_manager_new_api(app, admin_user, new_api_statistics):
assert cell.get_filter_params() == {}
@with_httmock(new_api_mock)
def test_chartng_cell_manager_subfilters(app, admin_user, new_api_statistics):
page = Page.objects.create(title='One', slug='index')
cell = ChartNgCell(page=page, order=1, placeholder='content')
cell.statistic = Statistic.objects.get(slug='with-subfilter')
cell.save()
app = login(app)
resp = app.get('/manage/pages/%s/' % page.id)
field_prefix = 'cdataviz_chartngcell-%s-' % cell.id
# choice with no subfilter
resp.form[field_prefix + 'form'] = 'contact'
resp = resp.form.submit().follow()
assert len(new_api_mock.call['requests']) == 1
assert 'menu' not in resp.form.fields
resp.form[field_prefix + 'form'] = 'error'
resp = resp.form.submit().follow()
assert len(new_api_mock.call['requests']) == 2
assert 'menu' not in resp.form.fields
# choice with subfilter
resp.form[field_prefix + 'form'] = 'food-request'
resp = resp.form.submit().follow()
assert len(new_api_mock.call['requests']) == 3
menu_field = resp.form[field_prefix + 'menu']
assert menu_field.value == ''
assert menu_field.options == [
('', True, '---------'),
('meat', False, 'Meat'),
('vegan', False, 'Vegan'),
]
resp.form[field_prefix + 'menu'] = 'meat'
resp = resp.form.submit().follow()
assert resp.form[field_prefix + 'menu'].value == 'meat'
cell.refresh_from_db()
assert cell.get_filter_params() == {'form': 'food-request', 'menu': 'meat'}
# choice with no subfilter
resp.form[field_prefix + 'form'] = 'contact'
resp = resp.form.submit().follow()
assert len(new_api_mock.call['requests']) == 4
assert 'menu' not in resp.form.fields
cell.refresh_from_db()
assert cell.get_filter_params() == {'form': 'contact'}
# changing another filter doesn't trigger request
resp.form[field_prefix + 'other'] = 'one'
resp = resp.form.submit().follow()
assert len(new_api_mock.call['requests']) == 4
@with_httmock(new_api_mock)
@pytest.mark.freeze_time('2021-10-06')
def test_chartng_cell_manager_new_api_time_range_templates(app, admin_user, new_api_statistics):