Projet

Général

Profil

0001-api-add-bookings-count-statistics-52846.patch

Valentin Deniaud, 20 avril 2021 15:46

Télécharger (9,47 ko)

Voir les différences:

Subject: [PATCH] api: add bookings count statistics (#52846)

 chrono/api/urls.py  |   2 +
 chrono/api/views.py | 120 ++++++++++++++++++++++++++++++++++++++++++--
 tests/test_api.py   |  79 +++++++++++++++++++++++++++++
 3 files changed, 197 insertions(+), 4 deletions(-)
chrono/api/urls.py
63 63
    url(r'^booking/(?P<booking_pk>\w+)/suspend/$', views.suspend_booking, name='api-suspend-booking'),
64 64
    url(r'^booking/(?P<booking_pk>\w+)/resize/$', views.resize_booking, name='api-resize-booking'),
65 65
    url(r'^booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics, name='api-booking-ics'),
66
    url(r'^statistics/$', views.statistics_list, name='api-statistics-list'),
67
    url(r'^statistics/bookings/$', views.bookings_statistics, name='api-statistics-bookings'),
66 68
]
chrono/api/views.py
20 20
import uuid
21 21

  
22 22
from django.db import transaction
23
from django.db.models import Prefetch, Q
23
from django.db.models import Count, Prefetch, Q
24
from django.db.models.functions import TruncDay
24 25
from django.http import Http404, HttpResponse
25 26
from django.shortcuts import get_object_or_404
26 27
from django.urls import reverse
......
36 37
from rest_framework.generics import ListAPIView
37 38
from rest_framework.views import APIView
38 39

  
40
from chrono.agendas.models import (
41
    Agenda,
42
    Booking,
43
    BookingColor,
44
    Category,
45
    Desk,
46
    Event,
47
    MeetingType,
48
    TimePeriodException,
49
)
39 50
from chrono.api.utils import APIError, Response
40

  
41
from ..agendas.models import Agenda, Booking, BookingColor, Desk, Event, MeetingType, TimePeriodException
42
from ..interval import IntervalSet
51
from chrono.interval import IntervalSet
43 52

  
44 53

  
45 54
def format_response_datetime(dt):
......
1812 1821

  
1813 1822

  
1814 1823
booking_ics = BookingICS.as_view()
1824

  
1825

  
1826
class StatisticsList(APIView):
1827
    permission_classes = (permissions.IsAuthenticated,)
1828

  
1829
    def get(self, request, *args, **kwargs):
1830
        categories = Category.objects.all()
1831
        category_options = [{'id': '_all', 'label': _('All')}] + [
1832
            {'id': x.slug, 'label': x.label} for x in categories
1833
        ]
1834
        return Response(
1835
            {
1836
                'data': [
1837
                    {
1838
                        'name': _('Bookings Count'),
1839
                        'url': request.build_absolute_uri(reverse('api-statistics-bookings')),
1840
                        'id': 'bookings_count',
1841
                        'filters': [
1842
                            {
1843
                                'id': 'time_interval',
1844
                                'label': _('Interval'),
1845
                                'options': [{'id': 'day', 'label': _('Day')}],
1846
                                'required': True,
1847
                                'default': 'month',
1848
                            },
1849
                            {
1850
                                'id': 'category',
1851
                                'label': _('Category'),
1852
                                'options': category_options,
1853
                                'required': False,
1854
                                'default': '_all',
1855
                            },
1856
                        ],
1857
                    }
1858
                ]
1859
            }
1860
        )
1861

  
1862

  
1863
statistics_list = StatisticsList.as_view()
1864

  
1865

  
1866
class StatisticsFiltersSerializer(serializers.Serializer):
1867
    time_interval = serializers.ChoiceField(choices=('day', _('Day')), default='day')
1868
    start = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
1869
    end = serializers.DateTimeField(required=False, input_formats=['iso-8601', '%Y-%m-%d'])
1870
    category = serializers.SlugField(required=False, allow_blank=False, max_length=256)
1871

  
1872

  
1873
class BookingsStatistics(APIView):
1874
    permission_classes = (permissions.IsAuthenticated,)
1875

  
1876
    def get(self, request, *args, **kwargs):
1877
        serializer = StatisticsFiltersSerializer(data=request.query_params)
1878
        if not serializer.is_valid():
1879
            raise APIError(
1880
                _('invalid statistics filters'),
1881
                err_class='invalid statistics filters',
1882
                errors=serializer.errors,
1883
                http_status=status.HTTP_400_BAD_REQUEST,
1884
            )
1885
        data = serializer.validated_data
1886

  
1887
        bookings = Booking.objects
1888
        if 'start' in data:
1889
            bookings = bookings.filter(creation_datetime__gte=data['start'])
1890
        if 'end' in data:
1891
            bookings = bookings.filter(creation_datetime__lte=data['end'])
1892

  
1893
        if 'category' in data and data['category'] != '_all':
1894
            bookings = bookings.filter(event__agenda__category__slug=data['category'])
1895

  
1896
        bookings = bookings.annotate(day=TruncDay('creation_datetime'))
1897
        bookings = bookings.values('day', 'user_was_present').annotate(total=Count('id')).order_by('day')
1898

  
1899
        bookings_by_day = collections.OrderedDict()
1900
        for booking in bookings:
1901
            totals_by_presence = bookings_by_day.setdefault(booking['day'], {})
1902
            totals_by_presence[booking['user_was_present']] = booking['total']
1903

  
1904
        bookings_by_presence = {None: [], True: [], False: []}
1905
        for bookings in bookings_by_day.values():
1906
            for presence, data in bookings_by_presence.items():
1907
                data.append(bookings.get(presence))
1908

  
1909
        labels = {None: _('Unknown'), True: _('Present'), False: _('Absent')}
1910
        series = [{'label': labels[k], 'data': data} for k, data in bookings_by_presence.items() if any(data)]
1911

  
1912
        if len(series) == 1 and series[0]['label'] == _('Unknown'):
1913
            series[0]['label'] = _('Bookings Count')
1914

  
1915
        return Response(
1916
            {
1917
                'data': {
1918
                    'x_labels': [day.strftime('%Y-%m-%d') for day in bookings_by_day],
1919
                    'series': series,
1920
                },
1921
                'err': 0,
1922
            }
1923
        )
1924

  
1925

  
1926
bookings_statistics = BookingsStatistics.as_view()
tests/test_api.py
6226 6226
    assert not event.in_bookable_period()
6227 6227
    resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
6228 6228
    assert len(resp.json['data']) == 0
6229

  
6230

  
6231
def test_statistics_list(app, user):
6232
    category_a = Category.objects.create(label='Category A')
6233
    category_b = Category.objects.create(label='Category B')
6234

  
6235
    # unauthorized
6236
    app.get('/api/statistics/', status=401)
6237

  
6238
    app.authorization = ('Basic', ('john.doe', 'password'))
6239
    resp = app.get('/api/statistics/')
6240
    category_filter = [x for x in resp.json['data'][0]['filters'] if x['id'] == 'category'][0]
6241
    assert len(category_filter['options']) == 3
6242

  
6243

  
6244
def test_statistics_bookings(app, user, freezer):
6245
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
6246
    event = Event.objects.create(start_datetime=now(), places=5, agenda=agenda)
6247

  
6248
    app.authorization = ('Basic', ('john.doe', 'password'))
6249
    resp = app.get('/api/statistics/')
6250
    url = [x for x in resp.json['data'] if x['id'] == 'bookings_count'][0]['url']
6251

  
6252
    resp = app.get(url)
6253
    assert len(resp.json['data']['series']) == 0
6254

  
6255
    freezer.move_to('2020-10-10')
6256
    for _ in range(10):
6257
        Booking.objects.create(event=event)
6258
    freezer.move_to('2020-10-15')
6259
    Booking.objects.create(event=event)
6260

  
6261
    resp = app.get(url + '?time_interval=day')
6262
    assert resp.json['data'] == {
6263
        'x_labels': ['2020-10-10', '2020-10-15'],
6264
        'series': [{'label': 'Bookings Count', 'data': [10, 1]}],
6265
    }
6266

  
6267
    # period filter
6268
    resp = app.get(url + '?start=2020-10-14&end=2020-10-16')
6269
    assert resp.json['data'] == {
6270
        'x_labels': ['2020-10-15'],
6271
        'series': [{'label': 'Bookings Count', 'data': [1]}],
6272
    }
6273

  
6274
    category = Category.objects.create(label='Category A', slug='category-a')
6275
    agenda = Agenda.objects.create(label='Foo bar', kind='events', category=category)
6276
    event = Event.objects.create(start_datetime=now(), places=5, agenda=agenda)
6277
    freezer.move_to('2020-10-25')
6278
    Booking.objects.create(event=event)
6279

  
6280
    # category filter
6281
    resp = app.get(url + '?category=category-a')
6282
    assert resp.json['data'] == {
6283
        'x_labels': ['2020-10-25'],
6284
        'series': [{'label': 'Bookings Count', 'data': [1]}],
6285
    }
6286

  
6287
    # invalid time_interval
6288
    resp = app.get(url + '?time_interval=month', status=400)
6289
    assert resp.json['err'] == 1
6290
    assert 'time_interval' in resp.json['errors']
6291

  
6292
    # absence/presence
6293
    for i in range(10):
6294
        Booking.objects.create(event=event, user_was_present=bool(i % 2))
6295

  
6296
    freezer.move_to('2020-11-01')
6297
    Booking.objects.create(event=event, user_was_present=True)
6298

  
6299
    resp = app.get(url)
6300
    assert resp.json['data'] == {
6301
        'x_labels': ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01'],
6302
        'series': [
6303
            {'label': 'Unknown', 'data': [10, 1, 1, None]},
6304
            {'label': 'Present', 'data': [None, None, 5, 1]},
6305
            {'label': 'Absent', 'data': [None, None, 5, None]},
6306
        ],
6307
    }
6229
-