api: add bookings count statistics (#52846)

This commit is contained in:
Valentin Deniaud 2021-04-14 14:19:05 +02:00
parent 8da8fd1396
commit 045ae53bf1
3 changed files with 190 additions and 5 deletions

View File

@ -68,4 +68,6 @@ urlpatterns = [
url(r'^booking/(?P<booking_pk>\w+)/suspend/$', views.suspend_booking, name='api-suspend-booking'),
url(r'^booking/(?P<booking_pk>\w+)/resize/$', views.resize_booking, name='api-resize-booking'),
url(r'^booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics, name='api-booking-ics'),
url(r'^statistics/$', views.statistics_list, name='api-statistics-list'),
url(r'^statistics/bookings/$', views.bookings_statistics, name='api-statistics-bookings'),
]

View File

@ -20,7 +20,8 @@ import itertools
import uuid
from django.db import transaction
from django.db.models import Prefetch, Q
from django.db.models import Count, Prefetch, Q
from django.db.models.functions import TruncDay
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.template import Context, Template
@ -37,19 +38,19 @@ from rest_framework.exceptions import ValidationError
from rest_framework.generics import ListAPIView
from rest_framework.views import APIView
from chrono.api.utils import APIError, Response
from ..agendas.models import (
from chrono.agendas.models import (
AbsenceReason,
Agenda,
Booking,
BookingColor,
Category,
Desk,
Event,
MeetingType,
TimePeriodException,
)
from ..interval import IntervalSet
from chrono.api.utils import APIError, Response
from chrono.interval import IntervalSet
def format_response_datetime(dt):
@ -1907,3 +1908,106 @@ class BookingICS(APIView):
booking_ics = BookingICS.as_view()
class StatisticsList(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, *args, **kwargs):
categories = Category.objects.all()
category_options = [{'id': '_all', 'label': _('All')}] + [
{'id': x.slug, 'label': x.label} for x in categories
]
return Response(
{
'data': [
{
'name': _('Bookings Count'),
'url': request.build_absolute_uri(reverse('api-statistics-bookings')),
'id': 'bookings_count',
'filters': [
{
'id': 'time_interval',
'label': _('Interval'),
'options': [{'id': 'day', 'label': _('Day')}],
'required': True,
'default': 'day',
},
{
'id': 'category',
'label': _('Category'),
'options': category_options,
'required': False,
'default': '_all',
},
],
}
]
}
)
statistics_list = StatisticsList.as_view()
class StatisticsFiltersSerializer(serializers.Serializer):
time_interval = serializers.ChoiceField(choices=('day', _('Day')), default='day')
start = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
end = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
category = serializers.SlugField(required=False, allow_blank=False, max_length=256)
class BookingsStatistics(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, *args, **kwargs):
serializer = StatisticsFiltersSerializer(data=request.query_params)
if not serializer.is_valid():
raise APIError(
_('invalid statistics filters'),
err_class='invalid statistics filters',
errors=serializer.errors,
http_status=status.HTTP_400_BAD_REQUEST,
)
data = serializer.validated_data
bookings = Booking.objects
if 'start' in data:
bookings = bookings.filter(creation_datetime__gte=data['start'])
if 'end' in data:
bookings = bookings.filter(creation_datetime__lte=data['end'])
if 'category' in data and data['category'] != '_all':
bookings = bookings.filter(event__agenda__category__slug=data['category'])
bookings = bookings.annotate(day=TruncDay('creation_datetime'))
bookings = bookings.values('day', 'user_was_present').annotate(total=Count('id')).order_by('day')
bookings_by_day = collections.OrderedDict()
for booking in bookings:
totals_by_presence = bookings_by_day.setdefault(booking['day'], {})
totals_by_presence[booking['user_was_present']] = booking['total']
bookings_by_presence = {None: [], True: [], False: []}
for bookings in bookings_by_day.values():
for presence, data in bookings_by_presence.items():
data.append(bookings.get(presence))
labels = {None: _('Unknown'), True: _('Present'), False: _('Absent')}
series = [{'label': labels[k], 'data': data} for k, data in bookings_by_presence.items() if any(data)]
if len(series) == 1 and series[0]['label'] == _('Unknown'):
series[0]['label'] = _('Bookings Count')
return Response(
{
'data': {
'x_labels': [day.strftime('%Y-%m-%d') for day in bookings_by_day],
'series': series,
},
'err': 0,
}
)
bookings_statistics = BookingsStatistics.as_view()

View File

@ -6484,3 +6484,82 @@ def test_recurring_events_api_exceptions(app, user, freezer):
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post(fillslot_url, status=400)
assert resp.json['err'] == 1
def test_statistics_list(app, user):
category_a = Category.objects.create(label='Category A')
category_b = Category.objects.create(label='Category B')
# unauthorized
app.get('/api/statistics/', status=401)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.get('/api/statistics/')
category_filter = [x for x in resp.json['data'][0]['filters'] if x['id'] == 'category'][0]
assert len(category_filter['options']) == 3
def test_statistics_bookings(app, user, freezer):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(start_datetime=now(), places=5, agenda=agenda)
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 len(resp.json['data']['series']) == 0
freezer.move_to('2020-10-10')
for _ in range(10):
Booking.objects.create(event=event)
freezer.move_to('2020-10-15')
Booking.objects.create(event=event)
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]}],
}
# 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]}],
}
category = Category.objects.create(label='Category A', slug='category-a')
agenda = Agenda.objects.create(label='Foo bar', kind='events', category=category)
event = Event.objects.create(start_datetime=now(), places=5, agenda=agenda)
freezer.move_to('2020-10-25')
Booking.objects.create(event=event)
# category filter
resp = app.get(url + '?category=category-a')
assert resp.json['data'] == {
'x_labels': ['2020-10-25'],
'series': [{'label': 'Bookings Count', 'data': [1]}],
}
# invalid time_interval
resp = app.get(url + '?time_interval=month', status=400)
assert resp.json['err'] == 1
assert 'time_interval' in resp.json['errors']
# absence/presence
for i in range(10):
Booking.objects.create(event=event, user_was_present=bool(i % 2))
freezer.move_to('2020-11-01')
Booking.objects.create(event=event, user_was_present=True)
resp = app.get(url)
assert resp.json['data'] == {
'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'],
'series': [
{'label': 'Unknown', 'data': [10, 1, 1, None]},
{'label': 'Present', 'data': [None, None, 5, 1]},
{'label': 'Absent', 'data': [None, None, 5, None]},
],
}