diff --git a/chrono/api/views.py b/chrono/api/views.py index a81abe11..b2178644 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -22,7 +22,7 @@ import json import uuid from django.db import IntegrityError, transaction -from django.db.models import BooleanField, Count, ExpressionWrapper, F, Prefetch, Q +from django.db.models import BooleanField, Count, ExpressionWrapper, F, Func, Prefetch, Q from django.db.models.expressions import RawSQL from django.db.models.functions import TruncDay from django.http import Http404, HttpResponse @@ -3055,12 +3055,6 @@ class StatisticsList(APIView): agenda_options = [{'id': '_all', 'label': pgettext('agendas', 'All')}] + [ {'id': x.slug, 'label': x.label} for x in agendas ] - booking_check_filters = set() - for agenda in Agenda.objects.exclude(booking_check_filters=''): - booking_check_filters.update(agenda.get_booking_check_filters()) - group_by_options = [{'id': 'user_was_present', 'label': _('Presence/Absence')}] + [ - {'id': x, 'label': x.capitalize()} for x in sorted(list(booking_check_filters)) - ] return Response( { 'data': [ @@ -3083,6 +3077,7 @@ class StatisticsList(APIView): 'options': category_options, 'required': True, 'default': '_all', + 'has_subfilters': True, }, { 'id': 'agenda', @@ -3090,13 +3085,7 @@ class StatisticsList(APIView): 'options': agenda_options, 'required': True, 'default': '_all', - }, - { - 'id': 'group_by', - 'label': _('Group by'), - 'options': group_by_options, - 'required': False, - 'multiple': True, + 'has_subfilters': True, }, ], } @@ -3117,6 +3106,7 @@ class BookingsStatistics(APIView): if not serializer.is_valid(): raise APIErrorBadRequest(N_('invalid statistics filters'), errors=serializer.errors) data = serializer.validated_data + subfilters = [] bookings = Booking.objects.filter(cancellation_datetime__isnull=True) if 'start' in data: @@ -3126,9 +3116,11 @@ class BookingsStatistics(APIView): if 'category' in data and data['category'] != '_all': bookings = bookings.filter(event__agenda__category__slug=data['category']) + subfilters = self.get_subfilters(agendas=Agenda.objects.filter(category__slug=data['category'])) if 'agenda' in data and data['agenda'] != '_all': bookings = bookings.filter(event__agenda__slug=data['agenda']) + subfilters = self.get_subfilters(agendas=Agenda.objects.filter(slug=data['agenda'])) bookings = bookings.annotate(day=TruncDay('event__start_datetime')) @@ -3191,10 +3183,33 @@ class BookingsStatistics(APIView): 'data': { 'x_labels': [day.strftime('%Y-%m-%d') for day in days], 'series': series, + 'subfilters': subfilters, }, 'err': 0, } ) + def get_subfilters(self, agendas): + extra_data_keys = ( + Booking.objects.filter(event__agenda__in=agendas) + .annotate(extra_data_keys=Func('extra_data', function='jsonb_object_keys')) + .distinct('extra_data_keys') + .order_by('extra_data_keys') + .values_list('extra_data_keys', flat=True) + ) + + group_by_options = [{'id': 'user_was_present', 'label': _('Presence/Absence')}] + [ + {'id': x, 'label': x.capitalize()} for x in extra_data_keys + ] + return [ + { + 'id': 'group_by', + 'label': _('Group by'), + 'options': group_by_options, + 'required': False, + 'multiple': True, + }, + ] + bookings_statistics = BookingsStatistics.as_view() diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py index b7242182..83a4612c 100644 --- a/tests/api/test_statistics.py +++ b/tests/api/test_statistics.py @@ -7,8 +7,8 @@ pytestmark = pytest.mark.django_db def test_statistics_list(app, user): - Agenda.objects.create(label='Foo bar', booking_check_filters='menu,allergies') - Agenda.objects.create(label='Bar foo', booking_check_filters='menu,special') + Agenda.objects.create(label='Foo bar') + Agenda.objects.create(label='Bar foo') Category.objects.create(label='Category A') Category.objects.create(label='Category B') @@ -21,13 +21,6 @@ def test_statistics_list(app, user): assert len(category_filter['options']) == 3 agenda_filter = [x for x in resp.json['data'][0]['filters'] if x['id'] == 'agenda'][0] assert len(agenda_filter['options']) == 3 - group_by_filter = [x for x in resp.json['data'][0]['filters'] if x['id'] == 'group_by'][0] - assert group_by_filter['options'] == [ - {'id': 'user_was_present', 'label': 'Presence/Absence'}, - {'id': 'allergies', 'label': 'Allergies'}, - {'id': 'menu', 'label': 'Menu'}, - {'id': 'special', 'label': 'Special'}, - ] def test_statistics_bookings(app, user, freezer): @@ -51,17 +44,13 @@ def test_statistics_bookings(app, user, freezer): Booking.objects.create(event=event2) resp = app.get(url + '?time_interval=day') - assert resp.json['data'] == { - 'x_labels': ['2020-10-10', '2020-10-15'], - 'series': [{'label': 'Bookings Count', 'data': [10, 1]}], - } + assert resp.json['data']['x_labels'] == ['2020-10-10', '2020-10-15'] + assert resp.json['data']['series'] == [{'label': 'Bookings Count', 'data': [10, 1]}] # period filter resp = app.get(url + '?start=2020-10-14&end=2020-10-16') - assert resp.json['data'] == { - 'x_labels': ['2020-10-15'], - 'series': [{'label': 'Bookings Count', 'data': [1]}], - } + assert resp.json['data']['x_labels'] == ['2020-10-15'] + assert resp.json['data']['series'] == [{'label': 'Bookings Count', 'data': [1]}] category = Category.objects.create(label='Category A', slug='category-a') agenda = Agenda.objects.create(label='Bar foo', kind='events', category=category) @@ -70,17 +59,13 @@ def test_statistics_bookings(app, user, freezer): # category filter resp = app.get(url + '?category=category-a') - assert resp.json['data'] == { - 'x_labels': ['2020-10-25'], - 'series': [{'label': 'Bookings Count', 'data': [1]}], - } + assert resp.json['data']['x_labels'] == ['2020-10-25'] + assert resp.json['data']['series'] == [{'label': 'Bookings Count', 'data': [1]}] # agenda filter resp = app.get(url + '?agenda=bar-foo') - assert resp.json['data'] == { - 'x_labels': ['2020-10-25'], - 'series': [{'label': 'Bookings Count', 'data': [1]}], - } + assert resp.json['data']['x_labels'] == ['2020-10-25'] + assert resp.json['data']['series'] == [{'label': 'Bookings Count', 'data': [1]}] # invalid time_interval resp = app.get(url + '?time_interval=month', status=400) @@ -95,19 +80,12 @@ def test_statistics_bookings(app, user, freezer): Booking.objects.create(event=event4, user_was_present=True) resp = app.get(url + '?group_by=user_was_present') - data = resp.json['data'] - assert data == { - 'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'], - 'series': [ - {'label': 'Absent', 'data': [None, None, 5, None]}, - {'label': 'Booked', 'data': [10, 1, 1, None]}, - {'label': 'Present', 'data': [None, None, 5, 1]}, - ], - } - - # any booking check filter - agenda.booking_check_filters = 'menu' - agenda.save() + assert resp.json['data']['x_labels'] == ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'] + assert resp.json['data']['series'] == [ + {'label': 'Absent', 'data': [None, None, 5, None]}, + {'label': 'Booked', 'data': [10, 1, 1, None]}, + {'label': 'Present', 'data': [None, None, 5, 1]}, + ] for i in range(9): Booking.objects.create( @@ -119,27 +97,66 @@ def test_statistics_bookings(app, user, freezer): ) resp = app.get(url + '?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': 'None', 'data': [10, 1, 11, 1]}, - {'label': 'meet', 'data': [None, None, 2, 3]}, - {'label': 'vegetables', 'data': [None, None, 4, 5]}, - ], - } + assert resp.json['data']['x_labels'] == ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'] + assert resp.json['data']['series'] == [ + {'label': 'None', 'data': [10, 1, 11, 1]}, + {'label': 'meet', 'data': [None, None, 2, 3]}, + {'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]}, - ], + assert resp.json['data']['x_labels'] == ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'] + assert resp.json['data']['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]}, + ] + + +def test_statistics_bookings_subfilters_list(app, user): + category = Category.objects.create(label='Category A') + agenda = Agenda.objects.create(label='Foo bar', kind='events', category=category) + + app.authorization = ('Basic', ('john.doe', 'password')) + resp = app.get('/api/statistics/') + url = [x for x in resp.json['data'] if x['id'] == 'bookings_count'][0]['url'] + + resp = app.get(url) + assert resp.json['data']['subfilters'] == [] + + resp = app.get(url + '?agenda=foo-bar') + assert len(resp.json['data']['subfilters']) == 1 + assert resp.json['data']['subfilters'][0]['id'] == 'group_by' + assert len(resp.json['data']['subfilters'][0]['options']) == 1 + assert resp.json['data']['subfilters'][0]['options'][0] == { + 'id': 'user_was_present', + 'label': 'Presence/Absence', } + + # extra data is shown in subfilters + event = Event.objects.create(start_datetime=now(), places=5, agenda=agenda) + Booking.objects.create(event=event, extra_data={'test': 'xxx'}) + + resp = app.get(url + '?agenda=foo-bar') + assert len(resp.json['data']['subfilters'][0]['options']) == 2 + assert resp.json['data']['subfilters'][0]['options'][0] == { + 'id': 'user_was_present', + 'label': 'Presence/Absence', + } + assert resp.json['data']['subfilters'][0]['options'][1] == {'id': 'test', 'label': 'Test'} + + resp = app.get(url + '?category=category-a') + assert len(resp.json['data']['subfilters'][0]['options']) == 2 + + Category.objects.create(label='Category B') + Agenda.objects.create(label='Other', kind='events', category=category) + + resp = app.get(url + '?agenda=other-agenda') + assert len(resp.json['data']['subfilters'][0]['options']) == 1 + + resp = app.get(url + '?category=category-b') + assert len(resp.json['data']['subfilters'][0]['options']) == 1