dataviz: add support for subfilters (#61083)
This commit is contained in:
parent
e42495fae1
commit
30d1483f03
|
@ -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'):
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -102,3 +102,4 @@ def refresh_statistics_data(cell_pk):
|
|||
except ChartNgCell.DoesNotExist:
|
||||
return
|
||||
cell.get_statistic_data(invalidate_cache=True)
|
||||
cell.update_subfilters()
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue