0001-api-allow-multiple-grouping-in-statistics-57817.patch
chrono/api/serializers.py | ||
---|---|---|
100 | 100 |
end = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d']) |
101 | 101 |
category = serializers.SlugField(required=False, allow_blank=False, max_length=256) |
102 | 102 |
agenda = serializers.SlugField(required=False, allow_blank=False, max_length=256) |
103 |
group_by = serializers.SlugField(required=False, allow_blank=False, max_length=256) |
|
103 |
group_by = serializers.ListField( |
|
104 |
required=False, child=serializers.SlugField(allow_blank=False, max_length=256) |
|
105 |
) |
|
104 | 106 | |
105 | 107 | |
106 | 108 |
class DateRangeSerializer(serializers.Serializer): |
chrono/api/views.py | ||
---|---|---|
31 | 31 |
from django.utils.encoding import force_text |
32 | 32 |
from django.utils.formats import date_format |
33 | 33 |
from django.utils.timezone import localtime, make_aware, now |
34 |
from django.utils.translation import gettext_noop |
|
34 |
from django.utils.translation import gettext, gettext_noop
|
|
35 | 35 |
from django.utils.translation import ugettext_lazy as _ |
36 | 36 |
from django_filters import rest_framework as filters |
37 | 37 |
from rest_framework import permissions, status |
... | ... | |
2460 | 2460 |
'label': _('Group by'), |
2461 | 2461 |
'options': group_by_options, |
2462 | 2462 |
'required': False, |
2463 |
'multiple': True, |
|
2463 | 2464 |
}, |
2464 | 2465 |
], |
2465 | 2466 |
} |
... | ... | |
2509 | 2510 |
series = [] |
2510 | 2511 |
else: |
2511 | 2512 |
group_by = data['group_by'] |
2512 |
if group_by not in ('user_was_present',): |
|
2513 |
group_by = 'extra_data__%s' % group_by |
|
2514 |
bookings = bookings.values('day', group_by).annotate(total=Count('id')).order_by('day') |
|
2513 |
if not isinstance(group_by, list): # legacy support |
|
2514 |
group_by = [group_by] |
|
2515 | ||
2516 |
lookups = [ |
|
2517 |
'extra_data__%s' % field if field != 'user_was_present' else field for field in group_by |
|
2518 |
] |
|
2519 |
bookings = bookings.values('day', *lookups).annotate(total=Count('id')).order_by('day') |
|
2515 | 2520 | |
2516 | 2521 |
days = bookings_by_day = collections.OrderedDict( |
2517 | 2522 |
# day1: {group1: total_11, group2: total_12}, |
... | ... | |
2522 | 2527 |
) |
2523 | 2528 |
for booking in bookings: |
2524 | 2529 |
totals_by_group = bookings_by_day.setdefault(booking['day'], {}) |
2525 |
group_value = booking[group_by]
|
|
2530 |
group_value = tuple(booking[field] for field in lookups)
|
|
2526 | 2531 |
totals_by_group[group_value] = booking['total'] |
2527 | 2532 |
seen_group_values.add(group_value) |
2528 | 2533 | |
... | ... | |
2533 | 2538 |
for group in seen_group_values: |
2534 | 2539 |
bookings_by_group[group] = [bookings.get(group) for bookings in bookings_by_day.values()] |
2535 | 2540 | |
2536 |
if group_by == 'user_was_present': |
|
2537 |
labels = {None: _('Booked'), True: _('Present'), False: _('Absent')} |
|
2538 |
series = [ |
|
2539 |
{'label': labels[k], 'data': data} for k, data in bookings_by_group.items() if any(data) |
|
2540 |
] |
|
2541 |
else: |
|
2542 |
series = [ |
|
2543 |
{'label': k or _('None'), 'data': data} |
|
2544 |
for k, data in bookings_by_group.items() |
|
2545 |
if any(data) |
|
2546 |
] |
|
2541 |
def build_label(group): |
|
2542 |
group_labels = [] |
|
2543 |
for field, value in zip(group_by, group): |
|
2544 |
if field == 'user_was_present': |
|
2545 |
label = {None: gettext('Booked'), True: gettext('Present'), False: gettext('Absent')}[ |
|
2546 |
value |
|
2547 |
] |
|
2548 |
else: |
|
2549 |
label = value or gettext('None') |
|
2550 |
group_labels.append(label) |
|
2551 |
return ' / '.join(group_labels) |
|
2552 | ||
2553 |
series = [ |
|
2554 |
{'label': build_label(k), 'data': data} for k, data in bookings_by_group.items() if any(data) |
|
2555 |
] |
|
2556 |
series.sort(key=lambda x: x['label']) |
|
2547 | 2557 | |
2548 | 2558 |
return Response( |
2549 | 2559 |
{ |
tests/api/test_statistics.py | ||
---|---|---|
95 | 95 | |
96 | 96 |
resp = app.get(url + '?group_by=user_was_present') |
97 | 97 |
data = resp.json['data'] |
98 |
data['series'].sort(key=lambda x: x['label']) |
|
99 | 98 |
assert data == { |
100 | 99 |
'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'], |
101 | 100 |
'series': [ |
... | ... | |
110 | 109 |
agenda.save() |
111 | 110 | |
112 | 111 |
for i in range(9): |
113 |
Booking.objects.create(event=event3 if i % 2 else event4, extra_data={'menu': 'vegetables'}) |
|
112 |
Booking.objects.create( |
|
113 |
event=event3 if i % 2 else event4, extra_data={'menu': 'vegetables'}, user_was_present=bool(i % 3) |
|
114 |
) |
|
114 | 115 |
for i in range(5): |
115 |
Booking.objects.create(event=event3 if i % 2 else event4, extra_data={'menu': 'meet'}) |
|
116 |
Booking.objects.create( |
|
117 |
event=event3 if i % 2 else event4, extra_data={'menu': 'meet'}, user_was_present=bool(i % 3) |
|
118 |
) |
|
116 | 119 | |
117 | 120 |
resp = app.get(url + '?group_by=menu') |
118 | 121 |
data = resp.json['data'] |
119 |
data['series'].sort(key=lambda x: x['label']) |
|
120 | 122 |
assert data == { |
121 | 123 |
'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'], |
122 | 124 |
'series': [ |
... | ... | |
125 | 127 |
{'label': 'vegetables', 'data': [None, None, 4, 5]}, |
126 | 128 |
], |
127 | 129 |
} |
130 | ||
131 |
resp = app.get(url + '?group_by=user_was_present&group_by=menu') |
|
132 |
data = resp.json['data'] |
|
133 |
assert data == { |
|
134 |
'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'], |
|
135 |
'series': [ |
|
136 |
{'label': 'Absent / None', 'data': [None, None, 5, None]}, |
|
137 |
{'label': 'Absent / meet', 'data': [None, None, 1, 1]}, |
|
138 |
{'label': 'Absent / vegetables', 'data': [None, None, 1, 2]}, |
|
139 |
{'label': 'Booked / None', 'data': [10, 1, 1, None]}, |
|
140 |
{'label': 'Present / None', 'data': [None, None, 5, 1]}, |
|
141 |
{'label': 'Present / meet', 'data': [None, None, 1, 2]}, |
|
142 |
{'label': 'Present / vegetables', 'data': [None, None, 3, 3]}, |
|
143 |
], |
|
144 |
} |
|
128 |
- |