Projet

Général

Profil

0002-dataviz-handle-new-api-to-get-statistics-from-elsewh.patch

Valentin Deniaud, 07 décembre 2020 16:40

Télécharger (18 ko)

Voir les différences:

Subject: [PATCH 2/3] dataviz: handle new api to get statistics from elsewhere
 (#48865)

 combo/apps/dataviz/__init__.py |  42 ++++---
 combo/apps/dataviz/models.py   |  56 ++++++---
 combo/settings.py              |   3 +
 tests/test_dataviz.py          | 221 +++++++++++++++++++++++++++++++++
 4 files changed, 290 insertions(+), 32 deletions(-)
combo/apps/dataviz/__init__.py
41 41
        if not settings.KNOWN_SERVICES:
42 42
            return
43 43

  
44
        statistics_providers = settings.STATISTICS_PROVIDERS + ['bijoe']
44 45
        start_update = timezone.now()
45
        bijoe_sites = settings.KNOWN_SERVICES.get('bijoe').items()
46
        for site_key, site_dict in bijoe_sites:
47
            result = requests.get('/visualization/json/',
48
                    remote_service=site_dict, without_user=True,
49
                    headers={'accept': 'application/json'}).json()
50
            for stat in result:
51
                Statistic.objects.update_or_create(
52
                    slug=stat['slug'],
53
                    site_slug=site_key,
54
                    service_slug='bijoe',
55
                    defaults={
56
                        'label': stat['name'],
57
                        'url': stat['data-url'],
58
                        'site_title': site_dict.get('title', ''),
59
                        'available': True,
60
                    }
61
                )
46
        for service in statistics_providers:
47
            sites = settings.KNOWN_SERVICES.get(service, {}).items()
48
            for site_key, site_dict in sites:
49
                if service == 'bijoe':
50
                    result = requests.get('/visualization/json/',
51
                            remote_service=site_dict, without_user=True,
52
                            headers={'accept': 'application/json'}).json()
53
                else:
54
                    result = requests.get('/api/statistics/',
55
                            remote_service=site_dict, without_user=True,
56
                            headers={'accept': 'application/json'}).json()['data']
57

  
58
                for stat in result:
59
                    Statistic.objects.update_or_create(
60
                        slug=stat.get('slug') or stat['id'],
61
                        site_slug=site_key,
62
                        service_slug=service,
63
                        defaults={
64
                            'label': stat['name'],
65
                            'url': stat.get('data-url') or stat['url'],
66
                            'site_title': site_dict.get('title', ''),
67
                            'available': True,
68
                        }
69
                    )
62 70
        Statistic.objects.filter(last_update__lt=start_update).update(available=False)
63 71

  
64 72

  
combo/apps/dataviz/models.py
177 177

  
178 178
    @classmethod
179 179
    def is_enabled(self):
180
        return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('bijoe')
180
        return settings.KNOWN_SERVICES.get('bijoe') or settings.STATISTICS_PROVIDERS
181 181

  
182 182
    def get_default_form_class(self):
183 183
        from .forms import ChartNgForm
......
202 202
            else:
203 203
                ctx['table'] = chart.render_table(
204 204
                    transpose=bool(chart.axis_count == 2),
205
                    total=chart.compute_sum,
205
                    total=getattr(chart, 'compute_sum', True),
206 206
                )
207 207
                ctx['table'] = ctx['table'].replace('<table>', '<table class="main">')
208 208
        return ctx
......
211 211
        response = requests.get(
212 212
                self.statistic.url,
213 213
                cache_duration=300,
214
                remote_service='auto',
215
                without_user=True,
214 216
                raise_if_not_cached=raise_if_not_cached)
215 217
        response.raise_for_status()
216 218
        response = response.json()
......
229 231
            'table': pygal.Bar,
230 232
            }[self.chart_type](config=pygal.Config(style=copy.copy(style)))
231 233

  
232
        x_labels, y_labels, data = self.parse_response(response, chart)
233
        chart.x_labels = x_labels
234
        self.prepare_chart(chart, width, height)
234
        if self.statistic.service_slug == 'bijoe':
235
            x_labels, y_labels, data = self.parse_response(response, chart)
236
            chart.x_labels = x_labels
237
            self.prepare_chart(chart, width, height)
235 238

  
236
        if chart.axis_count == 1:
237
            if self.hide_null_values:
238
                data = self.hide_values(chart, data)
239
            if self.sort_order != 'none':
240
                data = self.sort_values(chart, data)
241
            if chart.compute_sum and self.chart_type == 'table':
242
                data = self.add_total_to_line_table(chart, data)
243
        self.add_data_to_chart(chart, data, y_labels)
239
            if chart.axis_count == 1:
240
                data = self.process_one_dimensional_data(chart, data)
241
            self.add_data_to_chart(chart, data, y_labels)
242
        else:
243
            data = response['data']
244
            chart.x_labels = data['x_labels']
245
            chart.axis_count = min(len(data['series']), 2)
246
            self.prepare_chart(chart, width, height)
247

  
248
            if chart.axis_count == 1:
249
                data['series'][0]['data'] = self.process_one_dimensional_data(
250
                    chart, data['series'][0]['data']
251
                )
252
                if self.chart_type == 'pie':
253
                    data["series"] = [
254
                        {"label": label, "data": [data]}
255
                        for label, data in zip(chart.x_labels, data["series"][0]["data"])
256
                        if data
257
                    ]
258

  
259
            for serie in data['series']:
260
                chart.add(serie['label'], serie['data'])
244 261

  
245 262
        return chart
246 263

  
......
277 294
        else:
278 295
            chart.axis_count = 2
279 296

  
280
        chart.show_legend = bool(len(response['axis']) > 1)
281 297
        chart.compute_sum = bool(response.get('measure') == 'integer' and chart.axis_count > 0)
282 298

  
283 299
        formatter = self.get_value_formatter(response.get('unit'), response.get('measure'))
......
296 312
            chart.config.explicit_size = True
297 313
        chart.config.js = [os.path.join(settings.STATIC_URL, 'js/pygal-tooltips.js')]
298 314

  
315
        chart.show_legend = bool(chart.axis_count > 1)
299 316
        chart.truncate_legend = 30
300 317
        # matplotlib tab10 palette
301 318
        chart.config.style.colors = (
......
319 336
            if width and width < 500:
320 337
                chart.truncate_legend = 15
321 338

  
339
    def process_one_dimensional_data(self, chart, data):
340
        if self.hide_null_values:
341
            data = self.hide_values(chart, data)
342
        if self.sort_order != 'none':
343
            data = self.sort_values(chart, data)
344
        if getattr(chart, 'compute_sum', True) and self.chart_type == 'table':
345
            data = self.add_total_to_line_table(chart, data)
346
        return data
347

  
322 348
    @staticmethod
323 349
    def hide_values(chart, data):
324 350
        x_labels, new_data = zip(*[(label, value) for label, value in zip(chart.x_labels, data) if value])
......
340 366
    def add_total_to_line_table(chart, data):
341 367
        # workaround pygal
342 368
        chart.compute_sum = False
343
        data.append(sum(data))
369
        data.append(sum(x for x in data if x is not None))
344 370
        chart.x_labels.append(gettext('Total'))
345 371
        return data
346 372

  
combo/settings.py
350 350
# known services
351 351
KNOWN_SERVICES = {}
352 352

  
353
# known services exposing statistics
354
STATISTICS_PROVIDERS = []
355

  
353 356
# PWA Settings
354 357
PWA_VAPID_PUBLIK_KEY = None
355 358
PWA_VAPID_PRIVATE_KEY = None
tests/test_dataviz.py
219 219
        return {'content': json.dumps(response), 'request': request, 'status_code': 404}
220 220

  
221 221

  
222
STATISTICS_LIST = {
223
    'data': [
224
        {
225
            'url': 'https://authentic.example.com/api/statistics/one-serie/',
226
            'name': 'One serie stat',
227
            'id': 'one-serie',
228
        },
229
        {
230
            'url': 'https://authentic.example.com/api/statistics/two-series/',
231
            'name': 'Two series stat',
232
            'id': 'two-series',
233
        },
234
        {
235
            'url': 'https://authentic.example.com/api/statistics/no-data/',
236
            'name': 'No data stat',
237
            'id': 'no-data',
238
        },
239
        {
240
            'url': 'https://authentic.example.com/api/statistics/not-found/',
241
            'name': '404 not found stat',
242
            'id': 'not-found',
243
        },
244
    ]
245
}
246

  
247

  
248
def new_api_mock(url, request):
249
    if url.path == '/visualization/json/':  # nothing from bijoe
250
        return {'content': b'{}', 'request': request, 'status_code': 200}
251
    if url.path == '/api/statistics/':
252
        return {'content': json.dumps(STATISTICS_LIST), 'request': request, 'status_code': 200}
253
    if url.path == '/api/statistics/one-serie/':
254
        response = {
255
            'data': {
256
                'series': [
257
                    {'data': [None, 16, 2], 'label': 'Serie 1'},
258
                ],
259
                'x_labels': ['2020-10', '2020-11', '2020-12'],
260
            },
261
        }
262
        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
263
    if url.path == '/api/statistics/two-series/':
264
        response = {
265
            'data': {
266
                'series': [
267
                    {'data': [None, 16, 2], 'label': 'Serie 1'},
268
                    {'data': [2, 1, None], 'label': 'Serie 2'},
269
                ],
270
                'x_labels': ['2020-10', '2020-11', '2020-12'],
271
            },
272
        }
273
        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
274
    if url.path == '/api/statistics/no-data/':
275
        response = {
276
            'data': {
277
                'series': [
278
                ],
279
                'x_labels': [],
280
            },
281
        }
282
        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
283
    if url.path == '/api/statistics/not-found/':
284
        return {'content': b'', 'request': request, 'status_code': 404}
285

  
286

  
222 287
@pytest.fixture
223 288
@with_httmock(bijoe_mock)
224 289
def statistics(settings):
......
232 297
    assert Statistic.objects.count() == 11
233 298

  
234 299

  
300
@pytest.fixture
301
@with_httmock(new_api_mock)
302
def new_api_statistics(settings):
303
    settings.KNOWN_SERVICES = {
304
        'authentic': {
305
            'connection': {
306
                'title': 'Connection',
307
                'url': 'https://authentic.example.com',
308
                'secret': 'combo',
309
                'orig': 'combo',
310
            }
311
        }
312
    }
313
    settings.STATISTICS_PROVIDERS = ['authentic']
314
    appconfig = apps.get_app_config('dataviz')
315
    appconfig.hourly()
316
    assert Statistic.objects.count() == 4
317

  
318

  
235 319
@with_httmock(bijoe_mock)
236 320
def test_chartng_cell(app, statistics):
237 321
    page = Page(title='One', slug='index')
......
339 423
        chart = cell.get_chart()
340 424

  
341 425

  
426
@with_httmock(new_api_mock)
427
def test_chartng_cell_new_api(app, new_api_statistics):
428
    page = Page.objects.create(title='One', slug='index')
429
    cell = ChartNgCell(page=page, order=1)
430
    cell.statistic = Statistic.objects.get(slug='one-serie')
431
    cell.save()
432

  
433
    chart = cell.get_chart()
434
    assert chart.__class__.__name__ == 'Bar'
435
    assert chart.x_labels == ['2020-10', '2020-11', '2020-12']
436
    assert chart.raw_series == [([None, 16, 2], {'title': 'Serie 1'},)]
437

  
438
    cell.chart_type = 'pie'
439
    chart = cell.get_chart()
440
    assert chart.__class__.__name__ == 'Pie'
441
    assert chart.x_labels == ['2020-10', '2020-11', '2020-12']
442
    assert chart.raw_series == [
443
        ([16], {'title': '2020-11'}),
444
        ([2], {'title': '2020-12'}),
445
    ]
446

  
447
    cell.statistic = Statistic.objects.get(slug='two-series')
448
    cell.save()
449

  
450
    chart = cell.get_chart()
451
    assert chart.x_labels == ['2020-10', '2020-11', '2020-12']
452
    assert chart.raw_series == [([None, 16, 2], {'title': 'Serie 1'}), ([2, 1, None], {'title': 'Serie 2'})]
453

  
454
    cell.statistic = Statistic.objects.get(slug='no-data')
455
    cell.save()
456

  
457
    chart = cell.get_chart()
458
    assert chart.x_labels == []
459
    assert chart.raw_series == []
460

  
461
    cell.statistic = Statistic.objects.get(slug='not-found')
462
    cell.save()
463
    with pytest.raises(HTTPError):
464
        chart = cell.get_chart()
465

  
466

  
342 467
@with_httmock(bijoe_mock)
343 468
def test_chartng_cell_hide_null_values(app, statistics):
344 469
    page = Page(title='One', slug='index')
......
424 549
    ]
425 550

  
426 551

  
552
@with_httmock(new_api_mock)
553
def test_chartng_cell_hide_null_values_new_api(app, new_api_statistics):
554
    page = Page.objects.create(title='One', slug='index')
555
    cell = ChartNgCell(page=page, order=1, hide_null_values=True)
556
    cell.statistic = Statistic.objects.get(slug='one-serie')
557
    cell.hide_null_values = True
558
    cell.save()
559

  
560
    chart = cell.get_chart()
561
    assert chart.x_labels == ['2020-11', '2020-12']
562
    assert chart.raw_series == [([16, 2], {'title': 'Serie 1'},)]
563

  
564

  
427 565
@with_httmock(bijoe_mock)
428 566
def test_chartng_cell_sort_order_alpha(app, statistics):
429 567
    page = Page(title='One', slug='index')
......
594 732
    ]
595 733

  
596 734

  
735
@with_httmock(new_api_mock)
736
def test_chartng_cell_sort_order_new_api(app, new_api_statistics):
737
    page = Page.objects.create(title='One', slug='index')
738
    cell = ChartNgCell(page=page, order=1)
739
    cell.statistic = Statistic.objects.get(slug='one-serie')
740
    cell.sort_order = 'desc'
741
    cell.save()
742

  
743
    chart = cell.get_chart()
744
    assert chart.x_labels == ['2020-11', '2020-12', '2020-10']
745
    assert chart.raw_series == [([16, 2, None], {'title': 'Serie 1'},)]
746

  
747

  
597 748
@with_httmock(bijoe_mock)
598 749
def test_chartng_cell_view(app, normal_user, statistics):
599 750
    page = Page(title='One', slug='index')
......
698 849
    assert not 'cell' in resp.text
699 850

  
700 851

  
852
@with_httmock(new_api_mock)
853
def test_chartng_cell_view_new_api(app, normal_user, new_api_statistics):
854
    page = Page.objects.create(title='One', slug='index')
855
    cell = ChartNgCell(page=page, order=1, placeholder='content')
856
    cell.statistic = Statistic.objects.get(slug='one-serie')
857
    cell.save()
858

  
859
    location = '/api/dataviz/graph/%s/' % cell.id
860
    resp = app.get('/')
861
    assert 'min-height: 250px' in resp.text
862
    assert location in resp.text
863

  
864
    # table visualization
865
    cell.chart_type = 'table'
866
    cell.save()
867
    resp = app.get('/')
868
    assert '<td>18</td>' in resp.text
869

  
870
    # deleted visualization
871
    cell.statistic = Statistic.objects.get(slug='not-found')
872
    cell.save()
873
    resp = app.get(location)
874
    assert 'not found' in resp.text
875

  
876

  
701 877
@with_httmock(bijoe_mock)
702 878
def test_chartng_cell_manager(app, admin_user, statistics):
703 879
    page = Page(title='One', slug='index')
......
726 902
    assert 'Unavailable Stat' in resp.text
727 903

  
728 904

  
905
@with_httmock(new_api_mock)
906
def test_chartng_cell_manager_new_api(app, admin_user, new_api_statistics):
907
    page = Page.objects.create(title='One', slug='index')
908
    cell = ChartNgCell(page=page, order=1, placeholder='content')
909
    cell.statistic = Statistic.objects.get(slug='one-serie')
910
    cell.save()
911

  
912
    app = login(app)
913
    resp = app.get('/manage/pages/%s/' % page.id)
914
    statistics_field = resp.form['cdataviz_chartngcell-%s-statistic' % cell.id]
915
    assert len(statistics_field.options) == 5
916
    assert statistics_field.value == str(cell.statistic.pk)
917
    assert statistics_field.options[3][2] == 'Connection: One serie stat'
918

  
919

  
729 920
@with_httmock(bijoe_mock)
730 921
def test_table_cell(app, admin_user, statistics):
731 922
    page = Page(title='One', slug='index')
......
769 960
    assert resp.text.count('Total') == 0
770 961

  
771 962

  
963
@with_httmock(new_api_mock)
964
def test_table_cell_new_api(app, admin_user, new_api_statistics):
965
    page = Page.objects.create(title='One', slug='index')
966
    cell = ChartNgCell(page=page, order=1, placeholder='content')
967
    cell.statistic = Statistic.objects.get(slug='one-serie')
968
    cell.chart_type = 'table'
969
    cell.save()
970

  
971
    app = login(app)
972
    resp = app.get('/')
973
    assert resp.text.count('Total') == 1
974

  
975
    cell.statistic = Statistic.objects.get(slug='two-series')
976
    cell.save()
977
    resp = app.get('/')
978
    assert '21' in resp.text
979
    assert resp.text.count('Total') == 2
980

  
981

  
772 982
def test_dataviz_hourly_unavailable_statistic(freezer, statistics):
773 983
    all_stats_count = Statistic.objects.count()
774 984
    assert Statistic.objects.filter(available=True).count() == all_stats_count
......
853 1063
    assert cell.statistic.site_slug == 'plop'
854 1064
    assert cell.statistic.service_slug == 'bijoe'
855 1065
    assert cell.statistic.site_title == 'test'
1066

  
1067

  
1068
@with_httmock(new_api_mock)
1069
def test_dataviz_api_list_statistics(new_api_statistics):
1070
    statistic = Statistic.objects.get(slug='one-serie')
1071
    assert statistic.label == 'One serie stat'
1072
    assert statistic.site_slug == 'connection'
1073
    assert statistic.service_slug == 'authentic'
1074
    assert statistic.site_title == 'Connection'
1075
    assert statistic.url == 'https://authentic.example.com/api/statistics/one-serie/'
1076
    assert statistic.available
856
-