From d7c8186bdf4a7326601e433edc12ead0197c75de Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Wed, 13 Oct 2021 11:54:24 +0200 Subject: [PATCH] api: allow multiple grouping in statistics (#57817) --- chrono/api/serializers.py | 4 +++- chrono/api/views.py | 42 ++++++++++++++++++++++-------------- tests/api/test_statistics.py | 25 +++++++++++++++++---- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py index 198d9eb0..7026fe38 100644 --- a/chrono/api/serializers.py +++ b/chrono/api/serializers.py @@ -100,7 +100,9 @@ class StatisticsFiltersSerializer(serializers.Serializer): end = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d']) category = serializers.SlugField(required=False, allow_blank=False, max_length=256) agenda = serializers.SlugField(required=False, allow_blank=False, max_length=256) - group_by = serializers.SlugField(required=False, allow_blank=False, max_length=256) + group_by = serializers.ListField( + required=False, child=serializers.SlugField(allow_blank=False, max_length=256) + ) class DateRangeSerializer(serializers.Serializer): diff --git a/chrono/api/views.py b/chrono/api/views.py index 5f6e4da8..8c644300 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -31,7 +31,7 @@ from django.utils.dates import WEEKDAYS from django.utils.encoding import force_text from django.utils.formats import date_format from django.utils.timezone import localtime, make_aware, now -from django.utils.translation import gettext_noop +from django.utils.translation import gettext, gettext_noop from django.utils.translation import ugettext_lazy as _ from django_filters import rest_framework as filters from rest_framework import permissions, status @@ -2460,6 +2460,7 @@ class StatisticsList(APIView): 'label': _('Group by'), 'options': group_by_options, 'required': False, + 'multiple': True, }, ], } @@ -2509,9 +2510,13 @@ class BookingsStatistics(APIView): series = [] else: group_by = data['group_by'] - if group_by not in ('user_was_present',): - group_by = 'extra_data__%s' % group_by - bookings = bookings.values('day', group_by).annotate(total=Count('id')).order_by('day') + if not isinstance(group_by, list): # legacy support + group_by = [group_by] + + lookups = [ + 'extra_data__%s' % field if field != 'user_was_present' else field for field in group_by + ] + bookings = bookings.values('day', *lookups).annotate(total=Count('id')).order_by('day') days = bookings_by_day = collections.OrderedDict( # day1: {group1: total_11, group2: total_12}, @@ -2522,7 +2527,7 @@ class BookingsStatistics(APIView): ) for booking in bookings: totals_by_group = bookings_by_day.setdefault(booking['day'], {}) - group_value = booking[group_by] + group_value = tuple(booking[field] for field in lookups) totals_by_group[group_value] = booking['total'] seen_group_values.add(group_value) @@ -2533,17 +2538,22 @@ class BookingsStatistics(APIView): for group in seen_group_values: bookings_by_group[group] = [bookings.get(group) for bookings in bookings_by_day.values()] - if group_by == 'user_was_present': - labels = {None: _('Booked'), True: _('Present'), False: _('Absent')} - series = [ - {'label': labels[k], 'data': data} for k, data in bookings_by_group.items() if any(data) - ] - else: - series = [ - {'label': k or _('None'), 'data': data} - for k, data in bookings_by_group.items() - if any(data) - ] + def build_label(group): + group_labels = [] + for field, value in zip(group_by, group): + if field == 'user_was_present': + label = {None: gettext('Booked'), True: gettext('Present'), False: gettext('Absent')}[ + value + ] + else: + label = value or gettext('None') + group_labels.append(label) + return ' / '.join(group_labels) + + series = [ + {'label': build_label(k), 'data': data} for k, data in bookings_by_group.items() if any(data) + ] + series.sort(key=lambda x: x['label']) return Response( { diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py index fb4b55ff..30732c21 100644 --- a/tests/api/test_statistics.py +++ b/tests/api/test_statistics.py @@ -95,7 +95,6 @@ def test_statistics_bookings(app, user, freezer): resp = app.get(url + '?group_by=user_was_present') data = resp.json['data'] - data['series'].sort(key=lambda x: x['label']) assert data == { 'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'], 'series': [ @@ -110,13 +109,16 @@ def test_statistics_bookings(app, user, freezer): agenda.save() for i in range(9): - Booking.objects.create(event=event3 if i % 2 else event4, extra_data={'menu': 'vegetables'}) + Booking.objects.create( + event=event3 if i % 2 else event4, extra_data={'menu': 'vegetables'}, user_was_present=bool(i % 3) + ) for i in range(5): - Booking.objects.create(event=event3 if i % 2 else event4, extra_data={'menu': 'meet'}) + Booking.objects.create( + event=event3 if i % 2 else event4, extra_data={'menu': 'meet'}, user_was_present=bool(i % 3) + ) resp = app.get(url + '?group_by=menu') data = resp.json['data'] - data['series'].sort(key=lambda x: x['label']) assert data == { 'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'], 'series': [ @@ -125,3 +127,18 @@ def test_statistics_bookings(app, user, freezer): {'label': 'vegetables', 'data': [None, None, 4, 5]}, ], } + + resp = app.get(url + '?group_by=user_was_present&group_by=menu') + data = resp.json['data'] + assert data == { + 'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'], + 'series': [ + {'label': 'Absent / None', 'data': [None, None, 5, None]}, + {'label': 'Absent / meet', 'data': [None, None, 1, 1]}, + {'label': 'Absent / vegetables', 'data': [None, None, 1, 2]}, + {'label': 'Booked / None', 'data': [10, 1, 1, None]}, + {'label': 'Present / None', 'data': [None, None, 5, 1]}, + {'label': 'Present / meet', 'data': [None, None, 1, 2]}, + {'label': 'Present / vegetables', 'data': [None, None, 3, 3]}, + ], + } -- 2.30.2