0001-dataviz-add-week-filters-55417.patch
combo/apps/dataviz/forms.py | ||
---|---|---|
56 | 56 |
class ChartNgForm(forms.ModelForm): |
57 | 57 |
blank_choice = ('', '---------') |
58 | 58 |
time_intervals = ( |
59 |
('_week', _('Week')), |
|
59 | 60 |
('_month', _('Month')), |
60 | 61 |
('_year', _('Year')), |
61 | 62 |
('_weekday', _('Week day')), |
combo/apps/dataviz/migrations/0012_auto_20201126_1557.py | ||
---|---|---|
46 | 46 |
related_name='cells', |
47 | 47 |
to='dataviz.Statistic', |
48 | 48 |
verbose_name='Data', |
49 |
help_text='This list may take a few seconds to be updated, please refresh the page if an item is missing.', |
|
49 | 50 |
), |
50 | 51 |
), |
51 | 52 |
] |
combo/apps/dataviz/migrations/0016_auto_20201215_1624.py | ||
---|---|---|
4 | 4 | |
5 | 5 |
from django.db import migrations, models |
6 | 6 | |
7 |
from combo.apps.dataviz.models import TIME_FILTERS |
|
8 | ||
7 | 9 | |
8 | 10 |
class Migration(migrations.Migration): |
9 | 11 | |
... | ... | |
17 | 19 |
name='time_range', |
18 | 20 |
field=models.CharField( |
19 | 21 |
blank=True, |
20 |
choices=[ |
|
21 |
('current-year', 'Current year'), |
|
22 |
('previous-year', 'Previous year'), |
|
23 |
('current-month', 'Current month'), |
|
24 |
('previous-month', 'Previous month'), |
|
25 |
('range', 'Free range'), |
|
26 |
], |
|
22 |
choices=TIME_FILTERS, |
|
27 | 23 |
max_length=20, |
28 | 24 |
verbose_name='Filtering (time)', |
29 | 25 |
), |
combo/apps/dataviz/models.py | ||
---|---|---|
22 | 22 | |
23 | 23 |
import pygal |
24 | 24 |
import pygal.util |
25 |
from dateutil.relativedelta import relativedelta |
|
25 |
from dateutil.relativedelta import MO, relativedelta
|
|
26 | 26 |
from django.conf import settings |
27 | 27 |
from django.contrib.postgres.fields import JSONField |
28 | 28 |
from django.db import models, transaction |
... | ... | |
149 | 149 |
return (self.slug, self.site_slug, self.service_slug) |
150 | 150 | |
151 | 151 | |
152 |
TIME_FILTERS = ( |
|
153 |
('previous-year', _('Previous year')), |
|
154 |
('current-year', _('Current year')), |
|
155 |
('next-year', _('Next year')), |
|
156 |
('previous-month', _('Previous month')), |
|
157 |
('current-month', _('Current month')), |
|
158 |
('next-month', _('Next month')), |
|
159 |
('previous-week', _('Previous week')), |
|
160 |
('current-week', _('Current week')), |
|
161 |
('next-week', _('Next week')), |
|
162 |
('range', _('Free range')), |
|
163 |
) |
|
164 | ||
165 | ||
152 | 166 |
@register_cell_class |
153 | 167 |
class ChartNgCell(CellBase): |
154 |
TIME_FILTERS = ( |
|
155 |
('current-year', _('Current year')), |
|
156 |
('previous-year', _('Previous year')), |
|
157 |
('current-month', _('Current month')), |
|
158 |
('previous-month', _('Previous month')), |
|
159 |
('range', _('Free range')), |
|
160 |
) |
|
161 | ||
162 | 168 |
statistic = models.ForeignKey( |
163 | 169 |
verbose_name=_('Data'), |
164 | 170 |
to=Statistic, |
... | ... | |
176 | 182 |
_('Filtering (time)'), |
177 | 183 |
max_length=20, |
178 | 184 |
blank=True, |
179 |
choices=( |
|
180 |
('current-year', _('Current year')), |
|
181 |
('previous-year', _('Previous year')), |
|
182 |
('current-month', _('Current month')), |
|
183 |
('previous-month', _('Previous month')), |
|
184 |
('range', _('Free range')), |
|
185 |
), |
|
185 |
choices=TIME_FILTERS, |
|
186 | 186 |
) |
187 | 187 |
time_range_start = models.DateField(_('From'), null=True, blank=True) |
188 | 188 |
time_range_end = models.DateField(_('To'), null=True, blank=True) |
... | ... | |
368 | 368 |
elif self.time_range == 'previous-year': |
369 | 369 |
params['start'] = date(year=now.year - 1, month=1, day=1) |
370 | 370 |
params['end'] = date(year=now.year, month=1, day=1) |
371 |
elif self.time_range == 'next-year': |
|
372 |
params['start'] = date(year=now.year + 1, month=1, day=1) |
|
373 |
params['end'] = date(year=now.year + 2, month=1, day=1) |
|
371 | 374 |
elif self.time_range == 'current-month': |
372 | 375 |
params['start'] = date(year=now.year, month=now.month, day=1) |
373 | 376 |
elif self.time_range == 'previous-month': |
374 | 377 |
params['start'] = date(year=now.year, month=now.month - 1, day=1) |
375 | 378 |
params['end'] = date(year=now.year, month=now.month, day=1) |
379 |
elif self.time_range == 'next-month': |
|
380 |
params['start'] = date(year=now.year, month=now.month + 1, day=1) |
|
381 |
params['end'] = date(year=now.year, month=now.month + 2, day=1) |
|
382 |
elif self.time_range == 'current-week': |
|
383 |
params['start'] = now + relativedelta(weekday=MO(-1)) |
|
384 |
params['end'] = now + relativedelta(weekday=MO(+1), days=+1) |
|
385 |
elif self.time_range == 'previous-week': |
|
386 |
params['start'] = now + relativedelta(weekday=MO(-2)) |
|
387 |
params['end'] = now + relativedelta(weekday=MO(-1)) |
|
388 |
elif self.time_range == 'next-week': |
|
389 |
params['start'] = now + relativedelta(weekday=MO(+1), days=+1) |
|
390 |
params['end'] = now + relativedelta(weekday=MO(+2), days=+1) |
|
376 | 391 |
elif self.time_range == 'range': |
377 | 392 |
if self.time_range_start: |
378 | 393 |
params['start'] = self.time_range_start |
... | ... | |
571 | 586 |
dates = [datetime.strptime(label, '%Y-%m-%d') for label in data['x_labels']] |
572 | 587 |
min_date, max_date = min(dates), max(dates) |
573 | 588 | |
589 |
date_formats = { |
|
590 |
'day': 'd-m-Y', |
|
591 |
# Translators: This indicates week number followed by year, for example it can yield W2-2021. |
|
592 |
# First "W" is the first letter of the word "week" and should be translated accordingly, second |
|
593 |
# "W" and "o" are interpreted by Django's date filter and should be left as is. First W is |
|
594 |
# backslash escaped to prevent it from being interpreted, translators should refer to Django's |
|
595 |
# documentation in order to know if the new letter resulting of translation should be escaped or not. |
|
596 |
'_week': gettext('\WW-o'), |
|
597 |
'_month': 'm-Y', |
|
598 |
'_year': 'Y', |
|
599 |
'_weekday': 'l', |
|
600 |
} |
|
574 | 601 |
if interval == 'day': |
575 | 602 |
x_labels = [ |
576 |
(min_date + timedelta(days=i)).strftime('%d-%m-%Y')
|
|
603 |
format_date(min_date + timedelta(days=i), date_formats['day'])
|
|
577 | 604 |
for i in range((max_date - min_date).days + 1) |
578 | 605 |
] |
579 | 606 |
elif interval == '_month': |
580 | 607 |
month_difference = max_date.month - min_date.month + (max_date.year - min_date.year) * 12 |
581 | 608 |
x_labels = [ |
582 |
(min_date + relativedelta(months=i)).strftime('%m-%Y') for i in range(month_difference + 1) |
|
609 |
format_date(min_date + relativedelta(months=i), date_formats['_month']) |
|
610 |
for i in range(month_difference + 1) |
|
583 | 611 |
] |
584 | 612 |
elif interval == '_year': |
585 | 613 |
x_labels = [str(year) for year in range(min_date.year, max_date.year + 1)] |
586 | 614 |
elif interval == '_weekday': |
587 | 615 |
x_labels = [str(label) for label in WEEKDAYS.values()] |
616 |
elif interval == '_week': |
|
617 |
x_labels = [] |
|
618 |
date, last_date = min_date, max_date |
|
619 |
if min_date.weekday() > max_date.weekday(): |
|
620 |
last_date += relativedelta(weeks=1) |
|
621 |
while date <= last_date: |
|
622 |
x_labels.append(format_date(date, date_formats['_week'])) |
|
623 |
date += relativedelta(weeks=1) |
|
588 | 624 | |
589 | 625 |
aggregates = OrderedDict((label, [0] * len(series_data)) for label in x_labels) |
590 |
date_formats = {'day': 'd-m-Y', '_month': 'm-Y', '_year': 'Y', '_weekday': 'l'} |
|
591 | 626 |
for i, date in enumerate(dates): |
592 | 627 |
key = format_date(date, date_formats[interval]) |
593 | 628 |
for j in range(len(series_data)): |
tests/test_dataviz.py | ||
---|---|---|
351 | 351 |
} |
352 | 352 |
], |
353 | 353 |
}, |
354 |
{ |
|
355 |
'url': 'https://authentic.example.com/api/statistics/leap-week/', |
|
356 |
'name': 'Same week spanning two years', |
|
357 |
'id': 'leap-week', |
|
358 |
"filters": [ |
|
359 |
{ |
|
360 |
"default": "day", |
|
361 |
"id": "time_interval", |
|
362 |
"label": "Time interval", |
|
363 |
"options": [ |
|
364 |
{"id": "day", "label": "Day"}, |
|
365 |
], |
|
366 |
"required": True, |
|
367 |
} |
|
368 |
], |
|
369 |
}, |
|
354 | 370 |
] |
355 | 371 |
} |
356 | 372 | |
... | ... | |
396 | 412 |
}, |
397 | 413 |
} |
398 | 414 |
return {'content': json.dumps(response), 'request': request, 'status_code': 200} |
415 |
if url.path == '/api/statistics/leap-week/': |
|
416 |
response = { |
|
417 |
'data': { |
|
418 |
'series': [ |
|
419 |
{'data': [None, 1, 16, 2], 'label': 'Serie 1'}, |
|
420 |
], |
|
421 |
'x_labels': ['2020-12-30', '2020-12-31', '2021-01-01', '2021-01-02'], |
|
422 |
}, |
|
423 |
} |
|
424 |
return {'content': json.dumps(response), 'request': request, 'status_code': 200} |
|
399 | 425 | |
400 | 426 | |
401 | 427 |
@pytest.fixture |
... | ... | |
1120 | 1146 |
('day', False, 'Day'), |
1121 | 1147 |
('month', True, 'Month'), |
1122 | 1148 |
('year', False, 'Year'), |
1149 |
('_week', False, 'Week'), |
|
1123 | 1150 |
('_weekday', False, 'Week day'), |
1124 | 1151 |
] |
1125 | 1152 | |
... | ... | |
1346 | 1373 | |
1347 | 1374 | |
1348 | 1375 |
@with_httmock(new_api_mock) |
1349 |
def test_chartng_cell_new_api_filter_params(new_api_statistics, nocache, freezer): |
|
1376 |
@pytest.mark.parametrize('date', ['2020-03-02 12:01', '2020-03-05 12:01']) # Monday and Thursday |
|
1377 |
def test_chartng_cell_new_api_filter_params(new_api_statistics, nocache, freezer, date): |
|
1350 | 1378 |
page = Page.objects.create(title='One', slug='index') |
1351 | 1379 |
cell = ChartNgCell(page=page, order=1, placeholder='content') |
1352 | 1380 |
cell.statistic = Statistic.objects.get(slug='one-serie') |
... | ... | |
1364 | 1392 |
assert 'time_interval=month' in request.url |
1365 | 1393 |
assert 'ou=default' in request.url |
1366 | 1394 | |
1367 |
freezer.move_to('2020-03-02 12:01')
|
|
1395 |
freezer.move_to(date)
|
|
1368 | 1396 |
cell.time_range = 'previous-year' |
1369 | 1397 |
cell.save() |
1370 | 1398 |
chart = cell.get_chart() |
... | ... | |
1373 | 1401 |
assert 'ou=default' in request.url |
1374 | 1402 |
assert 'start=2019-01-01' in request.url and 'end=2020-01-01' in request.url |
1375 | 1403 | |
1404 |
cell.time_range = 'current-week' |
|
1405 |
cell.save() |
|
1406 |
chart = cell.get_chart() |
|
1407 |
request = new_api_mock.call['requests'][-1] |
|
1408 |
assert 'start=2020-03-02' in request.url and 'end=2020-03-09' in request.url |
|
1409 | ||
1410 |
cell.time_range = 'previous-week' |
|
1411 |
cell.save() |
|
1412 |
chart = cell.get_chart() |
|
1413 |
request = new_api_mock.call['requests'][-1] |
|
1414 |
assert 'start=2020-02-24' in request.url and 'end=2020-03-02' in request.url |
|
1415 | ||
1416 |
cell.time_range = 'next-week' |
|
1417 |
cell.save() |
|
1418 |
chart = cell.get_chart() |
|
1419 |
request = new_api_mock.call['requests'][-1] |
|
1420 |
assert 'start=2020-03-09' in request.url and 'end=2020-03-16' in request.url |
|
1421 | ||
1376 | 1422 |
cell.time_range = 'range' |
1377 | 1423 |
cell.save() |
1378 | 1424 |
chart = cell.get_chart() |
1379 |
request = new_api_mock.call['requests'][3]
|
|
1425 |
request = new_api_mock.call['requests'][-1]
|
|
1380 | 1426 |
assert 'start' not in urllib.parse.parse_qs(urllib.parse.urlparse(request.url).query) |
1381 | 1427 |
assert 'end' not in urllib.parse.parse_qs(urllib.parse.urlparse(request.url).query) |
1382 | 1428 | |
1383 | 1429 |
cell.time_range_start = '2020-10-01' |
1384 | 1430 |
cell.save() |
1385 | 1431 |
chart = cell.get_chart() |
1386 |
request = new_api_mock.call['requests'][4]
|
|
1432 |
request = new_api_mock.call['requests'][-1]
|
|
1387 | 1433 |
assert 'start=2020-10-01' in request.url |
1388 | 1434 | |
1389 | 1435 |
cell.time_range_end = '2020-11-03' |
1390 | 1436 |
cell.save() |
1391 | 1437 |
chart = cell.get_chart() |
1392 |
request = new_api_mock.call['requests'][5]
|
|
1438 |
request = new_api_mock.call['requests'][-1]
|
|
1393 | 1439 |
assert 'start=2020-10-01' in request.url and 'end=2020-11-03' in request.url |
1394 | 1440 | |
1395 | 1441 | |
... | ... | |
1435 | 1481 |
assert time_interval_field.value == 'day' |
1436 | 1482 |
assert time_interval_field.options == [ |
1437 | 1483 |
('day', True, 'Day'), |
1484 |
('_week', False, 'Week'), |
|
1438 | 1485 |
('_month', False, 'Month'), |
1439 | 1486 |
('_year', False, 'Year'), |
1440 | 1487 |
('_weekday', False, 'Week day'), |
... | ... | |
1486 | 1533 |
([16, 3, 0, 0, 0, 0, 0], {'title': 'Serie 1'}), |
1487 | 1534 |
([1, 4, 0, 0, 0, 0, 0], {'title': 'Serie 2'}), |
1488 | 1535 |
] |
1536 | ||
1537 |
time_interval_field.value = '_week' |
|
1538 |
resp.form.submit() |
|
1539 |
cell.refresh_from_db() |
|
1540 | ||
1541 |
chart = cell.get_chart() |
|
1542 |
assert 'time_interval=day' in new_api_mock.call['requests'][1].url |
|
1543 |
assert len(chart.x_labels) == 70 |
|
1544 |
assert chart.x_labels[:3] == ['W41-2020', 'W42-2020', 'W43-2020'] |
|
1545 |
assert chart.x_labels[-6:] == ['W52-2021', 'W1-2022', 'W2-2022', 'W3-2022', 'W4-2022', 'W5-2022'] |
|
1546 |
assert chart.raw_series == [ |
|
1547 |
([0, 1, 0, 0, 0, 0, 0, 0, 16] + [0] * 60 + [2], {'title': 'Serie 1'}), |
|
1548 |
([2, 2, 0, 0, 0, 0, 0, 0, 1] + [0] * 61, {'title': 'Serie 2'}), |
|
1549 |
] |
|
1550 | ||
1551 |
cell.statistic = Statistic.objects.get(slug='leap-week') |
|
1552 |
cell.save() |
|
1553 |
resp = app.get('/manage/pages/%s/' % page.id) |
|
1554 |
resp.form.submit() |
|
1555 |
chart = cell.get_chart() |
|
1556 |
assert 'time_interval=day' in new_api_mock.call['requests'][1].url |
|
1557 |
assert len(chart.x_labels) == 1 |
|
1558 |
assert chart.x_labels == ['W53-2020'] |
|
1559 |
assert chart.raw_series == [([19], {'title': 'Serie 1'})] |
|
1489 |
- |