Projet

Général

Profil

0001-dataviz-aggregate-received-data-by-time-intervals-53.patch

Valentin Deniaud, 20 avril 2021 12:34

Télécharger (10,1 ko)

Voir les différences:

Subject: [PATCH] dataviz: aggregate received data by time intervals (#53180)

 combo/apps/dataviz/forms.py  | 17 +++++++
 combo/apps/dataviz/models.py | 45 ++++++++++++++++-
 tests/test_dataviz.py        | 94 ++++++++++++++++++++++++++++++++++++
 3 files changed, 155 insertions(+), 1 deletion(-)
combo/apps/dataviz/forms.py
21 21
from django.conf import settings
22 22
from django.db import transaction
23 23
from django.db.models import Q
24
from django.utils.translation import ugettext_lazy as _
24 25

  
25 26
from combo.utils import cache_during_request, requests, spooler
26 27

  
......
54 55

  
55 56
class ChartNgForm(forms.ModelForm):
56 57
    blank_choice = ('', '---------')
58
    time_intervals = (
59
        ('_month', _('Month')),
60
        ('_year', _('Year')),
61
        ('_weekday', _('Week day')),
62
    )
57 63

  
58 64
    class Meta:
59 65
        model = ChartNgCell
......
109 115

  
110 116
        self.fields = OrderedDict((field_id, self.fields[field_id]) for field_id in field_ids)
111 117

  
118
        if 'time_interval' in self.fields:
119
            self.extend_time_interval_choices()
120

  
112 121
    def save(self, *args, **kwargs):
113 122
        if 'statistic' in self.changed_data:
114 123
            self.instance.filter_params.clear()
......
122 131
                else:
123 132
                    self.instance.filter_params.pop(field, None)
124 133
        return super().save(*args, **kwargs)
134

  
135
    def extend_time_interval_choices(self):
136
        choice_ids = {choice_id for choice_id, _ in self.fields['time_interval'].choices}
137
        if 'day' not in choice_ids:
138
            return
139
        for choice in self.time_intervals:
140
            if choice[0].strip('_') not in choice_ids:
141
                self.fields['time_interval'].choices.append(choice)
combo/apps/dataviz/models.py
17 17
import copy
18 18
import os
19 19
import sys
20
from datetime import date
20
from collections import OrderedDict
21
from datetime import date, datetime, timedelta
21 22

  
22 23
import pygal
23 24
import pygal.util
25
from dateutil.relativedelta import relativedelta
24 26
from django.conf import settings
25 27
from django.db import models, transaction
28
from django.template.defaultfilters import date as format_date
26 29
from django.urls import reverse
27 30
from django.utils import timezone
31
from django.utils.dates import WEEKDAYS
28 32
from django.utils.encoding import force_text
29 33
from django.utils.translation import gettext
30 34
from django.utils.translation import ugettext_lazy as _
......
324 328
            self.add_data_to_chart(chart, data, y_labels)
325 329
        else:
326 330
            data = response['data']
331

  
332
            interval = self.filter_params.get('time_interval', '')
333
            if interval == 'day' or interval.startswith('_'):
334
                self.aggregate_data(data, interval)
335

  
327 336
            chart.x_labels = data['x_labels']
328 337
            chart.axis_count = min(len(data['series']), 2)
329 338
            self.prepare_chart(chart, width, height)
......
347 356

  
348 357
        return chart
349 358

  
359
    @staticmethod
360
    def aggregate_data(data, interval):
361
        series_data = [serie['data'] for serie in data['series']]
362
        dates = [datetime.strptime(label, '%Y-%m-%d') for label in data['x_labels']]
363
        min_date, max_date = min(dates), max(dates)
364

  
365
        if interval == 'day':
366
            x_labels = [
367
                (min_date + timedelta(days=i)).strftime('%d-%m-%Y')
368
                for i in range((max_date - min_date).days + 1)
369
            ]
370
        elif interval == '_month':
371
            month_difference = max_date.month - min_date.month + (max_date.year - min_date.year) * 12
372
            x_labels = [
373
                (min_date + relativedelta(months=i)).strftime('%m-%Y') for i in range(month_difference + 1)
374
            ]
375
        elif interval == '_year':
376
            x_labels = [str(year) for year in range(min_date.year, max_date.year + 1)]
377
        elif interval == '_weekday':
378
            x_labels = [str(label) for label in WEEKDAYS.values()]
379

  
380
        aggregates = OrderedDict((label, [0] * len(series_data)) for label in x_labels)
381
        date_formats = {'day': 'd-m-Y', '_month': 'm-Y', '_year': 'Y', '_weekday': 'l'}
382
        for i, date in enumerate(dates):
383
            key = format_date(date, date_formats[interval])
384
            for j in range(len(series_data)):
385
                aggregates[key][j] += series_data[j][i] or 0
386

  
387
        data['x_labels'] = x_labels
388
        for i, serie in enumerate(data['series']):
389
            serie['data'] = [values[i] for values in aggregates.values()]
390

  
350 391
    def get_filter_params(self):
351 392
        params = self.filter_params.copy()
352 393
        now = timezone.now().date()
......
365 406
                params['start'] = self.time_range_start
366 407
            if self.time_range_end:
367 408
                params['end'] = self.time_range_end
409
        if 'time_interval' in params and params['time_interval'].startswith('_'):
410
            params['time_interval'] = 'day'
368 411
        return params
369 412

  
370 413
    def parse_response(self, response, chart):
tests/test_dataviz.py
318 318
            'name': '404 not found stat',
319 319
            'id': 'not-found',
320 320
        },
321
        {
322
            'url': 'https://authentic.example.com/api/statistics/daily/',
323
            'name': 'daily discontinuous serie',
324
            'id': 'daily',
325
            "filters": [
326
                {
327
                    "default": "day",
328
                    "id": "time_interval",
329
                    "label": "Time interval",
330
                    "options": [
331
                        {"id": "day", "label": "Day"},
332
                    ],
333
                    "required": True,
334
                }
335
            ],
336
        },
321 337
    ]
322 338
}
323 339

  
......
352 368
        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
353 369
    if url.path == '/api/statistics/not-found/':
354 370
        return {'content': b'', 'request': request, 'status_code': 404}
371
    if url.path == '/api/statistics/daily/':
372
        response = {
373
            'data': {
374
                'series': [
375
                    {'data': [None, 1, 16, 2], 'label': 'Serie 1'},
376
                    {'data': [2, 2, 1, None], 'label': 'Serie 2'},
377
                ],
378
                'x_labels': ['2020-10-06', '2020-10-13', '2020-11-30', '2022-02-01'],
379
            },
380
        }
381
        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
355 382

  
356 383

  
357 384
@pytest.fixture
......
1057 1084
        ('day', False, 'Day'),
1058 1085
        ('month', True, 'Month'),
1059 1086
        ('year', False, 'Year'),
1087
        ('_weekday', False, 'Week day'),
1060 1088
    ]
1061 1089

  
1062 1090
    ou_field = resp.form[field_prefix + 'ou']
......
1350 1378
        cell.check_validity()
1351 1379
    validity_info = ValidityInfo.objects.latest('pk')
1352 1380
    assert validity_info.invalid_reason_code == 'statistic_data_not_found'
1381

  
1382

  
1383
@with_httmock(new_api_mock)
1384
def test_chartng_cell_new_api_aggregation(new_api_statistics, app, admin_user, nocache):
1385
    page = Page.objects.create(title='One', slug='index')
1386
    cell = ChartNgCell(page=page, order=1, placeholder='content')
1387
    cell.statistic = Statistic.objects.get(slug='daily')
1388
    cell.save()
1389

  
1390
    app = login(app)
1391
    resp = app.get('/manage/pages/%s/' % page.id)
1392
    time_interval_field = resp.form['cdataviz_chartngcell-%s-time_interval' % cell.id]
1393
    assert time_interval_field.value == 'day'
1394
    assert time_interval_field.options == [
1395
        ('day', True, 'Day'),
1396
        ('_month', False, 'Month'),
1397
        ('_year', False, 'Year'),
1398
        ('_weekday', False, 'Week day'),
1399
    ]
1400
    resp.form.submit()
1401
    cell.refresh_from_db()
1402

  
1403
    chart = cell.get_chart()
1404
    assert len(chart.x_labels) == 484
1405
    assert chart.x_labels[:3] == ['06-10-2020', '07-10-2020', '08-10-2020']
1406
    assert chart.x_labels[-3:] == ['30-01-2022', '31-01-2022', '01-02-2022']
1407
    assert chart.raw_series[0][0][:8] == [0, 0, 0, 0, 0, 0, 0, 1]
1408
    assert chart.raw_series[1][0][:8] == [2, 0, 0, 0, 0, 0, 0, 2]
1409

  
1410
    time_interval_field.value = '_month'
1411
    resp.form.submit()
1412
    cell.refresh_from_db()
1413

  
1414
    chart = cell.get_chart()
1415
    assert 'time_interval=day' in new_api_mock.call['requests'][1].url
1416
    assert len(chart.x_labels) == 17
1417
    assert chart.x_labels[:3] == ['10-2020', '11-2020', '12-2020']
1418
    assert chart.x_labels[-3:] == ['12-2021', '01-2022', '02-2022']
1419
    assert chart.raw_series == [
1420
        ([1, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], {'title': 'Serie 1'}),
1421
        ([4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], {'title': 'Serie 2'}),
1422
    ]
1423

  
1424
    time_interval_field.value = '_year'
1425
    resp.form.submit()
1426
    cell.refresh_from_db()
1427

  
1428
    chart = cell.get_chart()
1429
    assert 'time_interval=day' in new_api_mock.call['requests'][2].url
1430
    assert chart.x_labels == ['2020', '2021', '2022']
1431
    assert chart.raw_series == [
1432
        ([17, 0, 2], {'title': 'Serie 1'}),
1433
        ([5, 0, 0], {'title': 'Serie 2'}),
1434
    ]
1435

  
1436
    time_interval_field.value = '_weekday'
1437
    resp.form.submit()
1438
    cell.refresh_from_db()
1439

  
1440
    chart = cell.get_chart()
1441
    assert 'time_interval=day' in new_api_mock.call['requests'][3].url
1442
    assert chart.x_labels == ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
1443
    assert chart.raw_series == [
1444
        ([16, 3, 0, 0, 0, 0, 0], {'title': 'Serie 1'}),
1445
        ([1, 4, 0, 0, 0, 0, 0], {'title': 'Serie 2'}),
1446
    ]
1353
-