0001-api-add-bookings-count-statistics-52846.patch
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 |
- |