0001-dataviz-add-support-for-subfilters-61083.patch
combo/apps/dataviz/forms.py | ||
---|---|---|
65 | 65 | |
66 | 66 |
def get_filter_fields(self, cell): |
67 | 67 |
fields = OrderedDict() |
68 |
for filter_ in cell.statistic.filters:
|
|
68 |
for filter_ in cell.available_filters:
|
|
69 | 69 |
filter_id = filter_['id'] |
70 | 70 |
choices = [(option['id'], option['label']) for option in filter_['options']] |
71 | 71 |
initial = cell.filter_params.get(filter_id, filter_.get('default')) |
... | ... | |
84 | 84 |
fields[filter_id] = field_class( |
85 | 85 |
label=filter_['label'], choices=choices, required=required, initial=initial |
86 | 86 |
) |
87 |
fields[filter_id].is_filter_field = True |
|
87 | 88 | |
88 | 89 |
# extend time interval choices if possible |
89 | 90 |
if 'time_interval' in fields: |
... | ... | |
139 | 140 |
stat_field.queryset = stat_field.queryset.filter( |
140 | 141 |
Q(available=True) | Q(pk=self.instance.statistic.pk) |
141 | 142 |
) |
143 |
self.add_filter_fields() |
|
142 | 144 | |
143 |
new_fields = OrderedDict() |
|
144 |
for field_name, field in self.fields.items(): |
|
145 |
new_fields[field_name] = field |
|
146 |
if field_name == 'statistic': |
|
147 |
# insert filter fields after statistic field |
|
148 |
new_fields.update(self.get_filter_fields(self.instance)) |
|
149 |
self.fields = new_fields |
|
145 |
def add_filter_fields(self): |
|
146 |
new_fields = OrderedDict() |
|
147 |
for field_name, field in self.fields.items(): |
|
148 |
new_fields[field_name] = field |
|
149 |
if field_name == 'statistic': |
|
150 |
# insert filter fields after statistic field |
|
151 |
new_fields.update(self.get_filter_fields(self.instance)) |
|
152 |
self.fields = new_fields |
|
150 | 153 | |
151 | 154 |
def save(self, *args, **kwargs): |
152 | 155 |
if 'statistic' in self.changed_data: |
153 | 156 |
self.instance.filter_params.clear() |
154 | 157 |
self.instance.time_range = '' |
155 |
for filter_ in self.instance.statistic.filters:
|
|
158 |
for filter_ in self.instance.available_filters:
|
|
156 | 159 |
if 'default' in filter_: |
157 | 160 |
self.instance.filter_params[filter_['id']] = filter_['default'] |
158 | 161 |
else: |
159 |
for filter_ in self.instance.statistic.filters:
|
|
162 |
for filter_ in self.instance.available_filters:
|
|
160 | 163 |
self.instance.filter_params[filter_['id']] = self.cleaned_data.get(filter_['id']) |
161 |
return super().save(*args, **kwargs) |
|
164 | ||
165 |
cell = super().save(*args, **kwargs) |
|
166 | ||
167 |
for filter_ in cell.available_filters: |
|
168 |
if filter_.get('has_subfilters') and filter_['id'] in self.changed_data: |
|
169 |
cell.update_subfilters() |
|
170 |
self.fields = OrderedDict( |
|
171 |
(name, field) |
|
172 |
for name, field in self.fields.items() |
|
173 |
if not hasattr(field, 'is_filter_field') |
|
174 |
) |
|
175 |
self.add_filter_fields() |
|
176 |
break |
|
177 | ||
178 |
return cell |
|
162 | 179 | |
163 | 180 |
def clean(self): |
164 | 181 |
for template_field in ('time_range_start_template', 'time_range_end_template'): |
combo/apps/dataviz/migrations/0022_chartngcell_subfilters.py | ||
---|---|---|
1 |
# Generated by Django 2.2.19 on 2022-01-25 16:21 |
|
2 | ||
3 |
import django.contrib.postgres.fields.jsonb |
|
4 |
from django.db import migrations |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('dataviz', '0021_chartfilterscell'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='chartngcell', |
|
16 |
name='subfilters', |
|
17 |
field=django.contrib.postgres.fields.jsonb.JSONField(default=list), |
|
18 |
), |
|
19 |
] |
combo/apps/dataviz/models.py | ||
---|---|---|
186 | 186 |
'This list may take a few seconds to be updated, please refresh the page if an item is missing.' |
187 | 187 |
), |
188 | 188 |
) |
189 |
subfilters = JSONField(default=list) |
|
189 | 190 |
filter_params = JSONField(default=dict) |
190 | 191 |
title = models.CharField(_('Title'), max_length=150, blank=True) |
191 | 192 |
time_range = models.CharField( |
... | ... | |
679 | 680 |
for i, serie in enumerate(data['series']): |
680 | 681 |
serie['data'] = [values[i] for values in aggregates.values()] |
681 | 682 | |
683 |
@property |
|
684 |
def available_filters(self): |
|
685 |
return self.statistic.filters + self.subfilters |
|
686 | ||
687 |
def update_subfilters(self): |
|
688 |
response = self.get_statistic_data() |
|
689 |
try: |
|
690 |
response.raise_for_status() |
|
691 |
data = response.json()['data'] |
|
692 |
except Exception: |
|
693 |
return |
|
694 | ||
695 |
new_subfilters = data.get('subfilters', []) |
|
696 |
if self.subfilters != new_subfilters: |
|
697 |
self.subfilters = new_subfilters |
|
698 |
subfilter_ids = {filter_['id'] for filter_ in self.available_filters} |
|
699 |
self.filter_params = {k: v for k, v in self.filter_params.items() if k in subfilter_ids} |
|
700 |
self.save() |
|
701 | ||
682 | 702 | |
683 | 703 |
@register_cell_class |
684 | 704 |
class ChartFiltersCell(CellBase): |
combo/utils/spooler.py | ||
---|---|---|
102 | 102 |
except ChartNgCell.DoesNotExist: |
103 | 103 |
return |
104 | 104 |
cell.get_statistic_data(invalidate_cache=True) |
105 |
cell.update_subfilters() |
tests/test_dataviz.py | ||
---|---|---|
407 | 407 |
} |
408 | 408 |
], |
409 | 409 |
}, |
410 |
{ |
|
411 |
'url': 'https://authentic.example.com/api/statistics/with-subfilter/', |
|
412 |
'name': 'With subfilter', |
|
413 |
'id': 'with-subfilter', |
|
414 |
'filters': [ |
|
415 |
{ |
|
416 |
'id': 'form', |
|
417 |
'label': 'Form', |
|
418 |
'has_subfilters': True, |
|
419 |
'options': [ |
|
420 |
{'id': 'food-request', 'label': 'Food request'}, |
|
421 |
{'id': 'contact', 'label': 'Contact'}, |
|
422 |
{'id': 'error', 'label': 'Error'}, |
|
423 |
], |
|
424 |
}, |
|
425 |
{ |
|
426 |
'id': 'other', |
|
427 |
'label': 'Other', |
|
428 |
'options': [ |
|
429 |
{'id': 'one', 'label': 'One'}, |
|
430 |
{'id': 'two', 'label': 'two'}, |
|
431 |
], |
|
432 |
}, |
|
433 |
], |
|
434 |
}, |
|
410 | 435 |
] |
411 | 436 |
} |
412 | 437 | |
... | ... | |
473 | 498 |
}, |
474 | 499 |
} |
475 | 500 |
return {'content': json.dumps(response), 'request': request, 'status_code': 200} |
501 |
if url.path == '/api/statistics/with-subfilter/': |
|
502 |
response = { |
|
503 |
'data': { |
|
504 |
'series': [{'data': [None, 16, 2], 'label': 'Serie 1'}], |
|
505 |
'x_labels': ['2020-10', '2020-11', '2020-12'], |
|
506 |
'subfilters': [], |
|
507 |
}, |
|
508 |
} |
|
509 |
if 'form=food-request' in url.query: |
|
510 |
response['data']['subfilters'] = [ |
|
511 |
{ |
|
512 |
"id": "menu", |
|
513 |
"label": "Menu", |
|
514 |
"options": [ |
|
515 |
{"id": "meat", "label": "Meat"}, |
|
516 |
{"id": "vegan", "label": "Vegan"}, |
|
517 |
], |
|
518 |
} |
|
519 |
] |
|
520 |
if 'form=error' in url.query: |
|
521 |
return {'content': b'', 'request': request, 'status_code': 404} |
|
522 |
return {'content': json.dumps(response), 'request': request, 'status_code': 200} |
|
476 | 523 | |
477 | 524 | |
478 | 525 |
@pytest.fixture |
... | ... | |
1320 | 1367 |
assert cell.get_filter_params() == {} |
1321 | 1368 | |
1322 | 1369 | |
1370 |
@with_httmock(new_api_mock) |
|
1371 |
def test_chartng_cell_manager_subfilters(app, admin_user, new_api_statistics): |
|
1372 |
page = Page.objects.create(title='One', slug='index') |
|
1373 |
cell = ChartNgCell(page=page, order=1, placeholder='content') |
|
1374 |
cell.statistic = Statistic.objects.get(slug='with-subfilter') |
|
1375 |
cell.save() |
|
1376 | ||
1377 |
app = login(app) |
|
1378 |
resp = app.get('/manage/pages/%s/' % page.id) |
|
1379 |
field_prefix = 'cdataviz_chartngcell-%s-' % cell.id |
|
1380 | ||
1381 |
# choice with no subfilter |
|
1382 |
resp.form[field_prefix + 'form'] = 'contact' |
|
1383 |
resp = resp.form.submit().follow() |
|
1384 | ||
1385 |
assert len(new_api_mock.call['requests']) == 1 |
|
1386 |
assert 'menu' not in resp.form.fields |
|
1387 | ||
1388 |
resp.form[field_prefix + 'form'] = 'error' |
|
1389 |
resp = resp.form.submit().follow() |
|
1390 | ||
1391 |
assert len(new_api_mock.call['requests']) == 2 |
|
1392 |
assert 'menu' not in resp.form.fields |
|
1393 | ||
1394 |
# choice with subfilter |
|
1395 |
resp.form[field_prefix + 'form'] = 'food-request' |
|
1396 |
resp = resp.form.submit().follow() |
|
1397 | ||
1398 |
assert len(new_api_mock.call['requests']) == 3 |
|
1399 |
menu_field = resp.form[field_prefix + 'menu'] |
|
1400 |
assert menu_field.value == '' |
|
1401 |
assert menu_field.options == [ |
|
1402 |
('', True, '---------'), |
|
1403 |
('meat', False, 'Meat'), |
|
1404 |
('vegan', False, 'Vegan'), |
|
1405 |
] |
|
1406 | ||
1407 |
resp.form[field_prefix + 'menu'] = 'meat' |
|
1408 |
resp = resp.form.submit().follow() |
|
1409 |
assert resp.form[field_prefix + 'menu'].value == 'meat' |
|
1410 |
cell.refresh_from_db() |
|
1411 |
assert cell.get_filter_params() == {'form': 'food-request', 'menu': 'meat'} |
|
1412 | ||
1413 |
# choice with no subfilter |
|
1414 |
resp.form[field_prefix + 'form'] = 'contact' |
|
1415 |
resp = resp.form.submit().follow() |
|
1416 | ||
1417 |
assert len(new_api_mock.call['requests']) == 4 |
|
1418 |
assert 'menu' not in resp.form.fields |
|
1419 |
cell.refresh_from_db() |
|
1420 |
assert cell.get_filter_params() == {'form': 'contact'} |
|
1421 | ||
1422 |
# changing another filter doesn't trigger request |
|
1423 |
resp.form[field_prefix + 'other'] = 'one' |
|
1424 |
resp = resp.form.submit().follow() |
|
1425 |
assert len(new_api_mock.call['requests']) == 4 |
|
1426 | ||
1427 | ||
1323 | 1428 |
@with_httmock(new_api_mock) |
1324 | 1429 |
@pytest.mark.freeze_time('2021-10-06') |
1325 | 1430 |
def test_chartng_cell_manager_new_api_time_range_templates(app, admin_user, new_api_statistics): |
1326 |
- |