From 964eb2f9e4bbd1e94e76494949c5e702b4758ca1 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 24 Jan 2022 18:13:24 +0100 Subject: [PATCH] dataviz: add support for subfilters (#61083) --- combo/apps/dataviz/forms.py | 17 ++- .../migrations/0022_chartngcell_subfilters.py | 19 ++++ combo/apps/dataviz/models.py | 20 ++++ combo/utils/spooler.py | 1 + tests/test_dataviz.py | 105 ++++++++++++++++++ 5 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 combo/apps/dataviz/migrations/0022_chartngcell_subfilters.py diff --git a/combo/apps/dataviz/forms.py b/combo/apps/dataviz/forms.py index 28b5b3b7..00cbaee8 100644 --- a/combo/apps/dataviz/forms.py +++ b/combo/apps/dataviz/forms.py @@ -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')) @@ -147,13 +147,22 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm): 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.__init__() + break + + return cell def clean(self): for template_field in ('time_range_start_template', 'time_range_end_template'): diff --git a/combo/apps/dataviz/migrations/0022_chartngcell_subfilters.py b/combo/apps/dataviz/migrations/0022_chartngcell_subfilters.py new file mode 100644 index 00000000..5f69ce86 --- /dev/null +++ b/combo/apps/dataviz/migrations/0022_chartngcell_subfilters.py @@ -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), + ), + ] diff --git a/combo/apps/dataviz/models.py b/combo/apps/dataviz/models.py index d75c0585..fcc2228c 100644 --- a/combo/apps/dataviz/models.py +++ b/combo/apps/dataviz/models.py @@ -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: + 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): diff --git a/combo/utils/spooler.py b/combo/utils/spooler.py index 347cdc58..8edbb95e 100644 --- a/combo/utils/spooler.py +++ b/combo/utils/spooler.py @@ -102,3 +102,4 @@ def refresh_statistics_data(cell_pk): except ChartNgCell.DoesNotExist: return cell.get_statistic_data(invalidate_cache=True) + cell.update_subfilters() diff --git a/tests/test_dataviz.py b/tests/test_dataviz.py index 32b7db8a..78555f40 100644 --- a/tests/test_dataviz.py +++ b/tests/test_dataviz.py @@ -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 @@ -1296,6 +1343,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): -- 2.30.2