Projet

Général

Profil

0001-dataviz-add-support-for-subfilters-61083.patch

Valentin Deniaud, 15 février 2022 17:11

Télécharger (10,9 ko)

Voir les différences:

Subject: [PATCH] dataviz: add support for subfilters (#61083)

 combo/apps/dataviz/forms.py                   |  39 +++++--
 .../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, 173 insertions(+), 11 deletions(-)
 create mode 100644 combo/apps/dataviz/migrations/0022_chartngcell_subfilters.py
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
-