api: use serializer for event datetimes api (#56083)

This commit is contained in:
Valentin Deniaud 2021-08-09 11:54:05 +02:00
parent 95e2618863
commit 8f127f3606
4 changed files with 90 additions and 86 deletions

View File

@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from chrono.agendas.models import Booking
from chrono.agendas.models import AbsenceReason, Booking
class StringOrListField(serializers.ListField):
@ -91,3 +91,30 @@ class StatisticsFiltersSerializer(serializers.Serializer):
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)
class DateRangeSerializer(serializers.Serializer):
datetime_formats = ['%Y-%m-%d', '%Y-%m-%d %H:%M', 'iso-8601']
date_start = serializers.DateTimeField(required=False, input_formats=datetime_formats)
date_end = serializers.DateTimeField(required=False, input_formats=datetime_formats)
class DatetimesSerializer(DateRangeSerializer):
min_places = serializers.IntegerField(min_value=1, default=1)
user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
exclude_user_external_id = serializers.CharField(required=False, max_length=250, allow_blank=True)
events = serializers.CharField(required=False, max_length=32, allow_blank=True)
hide_disabled = serializers.BooleanField(default=False)
def validate(self, attrs):
super().validate(attrs)
if (
'user_external_id' in attrs
and 'exclude_user_external_id' in attrs
and attrs['user_external_id'] != attrs['exclude_user_external_id']
):
raise ValidationError(
{'user_external_id': _('user_external_id and exclude_user_external_id have different values')}
)
return attrs

View File

@ -27,11 +27,10 @@ from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
from django.urls import reverse
from django.utils.dateparse import parse_date, parse_datetime
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 is_naive, localtime, make_aware, now
from django.utils.timezone import localtime, make_aware, now
from django.utils.translation import gettext_noop
from django.utils.translation import ugettext_lazy as _
from django_filters import rest_framework as filters
@ -41,7 +40,6 @@ from rest_framework.generics import ListAPIView
from rest_framework.views import APIView
from chrono.agendas.models import (
AbsenceReason,
Agenda,
Booking,
BookingColor,
@ -611,42 +609,17 @@ def get_resources_from_request(request, agenda):
return resources
def parse_date_or_datetime(value):
try:
result = parse_datetime(value)
if result:
if is_naive(result):
return make_aware(result)
return result
result = parse_date(value)
if result:
return make_aware(datetime.datetime.combine(result, datetime.time(0, 0)))
except ValueError:
return None
return None
def get_start_and_end_datetime_from_request(request):
start_datetime, end_datetime = request.GET.get('date_start'), request.GET.get('date_end')
if start_datetime:
start_datetime = parse_date_or_datetime(start_datetime)
if not start_datetime:
raise APIError(
_('date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'),
err_class='date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM',
http_status=status.HTTP_400_BAD_REQUEST,
)
if end_datetime:
end_datetime = parse_date_or_datetime(end_datetime)
if not end_datetime:
raise APIError(
_('date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'),
err_class='date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM',
http_status=status.HTTP_400_BAD_REQUEST,
)
return start_datetime, end_datetime
serializer = serializers.DateRangeSerializer(data=request.query_params)
if not serializer.is_valid():
raise APIError(
_('invalid payload'),
err_class='invalid payload',
errors=serializer.errors,
http_status=status.HTTP_400_BAD_REQUEST,
)
return serializer.validated_data.get('date_start'), serializer.validated_data.get('date_end')
def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_list=False, color=None):
@ -771,6 +744,7 @@ agenda_detail = AgendaDetail.as_view()
class Datetimes(APIView):
permission_classes = ()
serializer_class = serializers.DatetimesSerializer
def get(self, request, agenda_identifier=None, format=None):
try:
@ -784,30 +758,18 @@ class Datetimes(APIView):
if agenda.kind != 'events':
raise Http404('agenda found, but it was not an events agenda')
try:
min_places = int(request.GET.get('min_places', 1))
except ValueError:
serializer = self.serializer_class(data=request.query_params)
if not serializer.is_valid():
raise APIError(
_('min_places must be a number'),
err_class='min_places must be a number',
_('invalid payload'),
err_class='invalid payload',
errors=serializer.errors,
http_status=status.HTTP_400_BAD_REQUEST,
)
payload = serializer.validated_data
start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
booked_user_external_id = request.GET.get('user_external_id') or None
excluded_user_external_id = request.GET.get('exclude_user_external_id') or None
if (
booked_user_external_id
and excluded_user_external_id
and booked_user_external_id != excluded_user_external_id
):
raise APIError(
_('user_external_id and exclude_user_external_id have different values'),
err_class='user_external_id and exclude_user_external_id have different values',
http_status=status.HTTP_400_BAD_REQUEST,
)
show_events_raw = request.GET.get('events')
user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
show_events_raw = payload.get('events')
show_events = show_events_raw or 'future'
show_past = show_events in ['all', 'past']
show_future = show_events in ['all', 'future']
@ -816,20 +778,20 @@ class Datetimes(APIView):
if show_past:
entries += agenda.get_past_events(
annotate_queryset=True,
min_start=start_datetime,
max_start=end_datetime,
user_external_id=booked_user_external_id or excluded_user_external_id,
min_start=payload.get('date_start'),
max_start=payload.get('date_end'),
user_external_id=user_external_id,
)
if show_future:
entries += agenda.get_open_events(
annotate_queryset=True,
min_start=start_datetime,
max_start=end_datetime,
user_external_id=booked_user_external_id or excluded_user_external_id,
min_start=payload.get('date_start'),
max_start=payload.get('date_end'),
user_external_id=user_external_id,
)
if request.GET.get('hide_disabled'):
entries = [e for e in entries if not is_event_disabled(e, min_places)]
if payload['hide_disabled']:
entries = [e for e in entries if not is_event_disabled(e, payload['min_places'])]
response = {
'data': [
@ -837,14 +799,14 @@ class Datetimes(APIView):
request,
x,
agenda=agenda,
min_places=min_places,
booked_user_external_id=booked_user_external_id,
min_places=payload['min_places'],
booked_user_external_id=payload.get('user_external_id'),
show_events=show_events_raw,
)
for x in entries
],
'meta': get_events_meta_detail(
request, entries, agenda=agenda, min_places=min_places, show_events=show_events_raw
request, entries, agenda=agenda, min_places=payload['min_places'], show_events=show_events_raw
),
}
return Response(response)

View File

@ -171,10 +171,10 @@ def test_datetime_api_min_places(app):
resp = app.get('/api/agenda/%s/datetimes/?min_places=5' % agenda.slug)
assert resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?min_places=wrong' % agenda.slug, status=400)
assert resp.json['err'] == 1
resp = app.get('/api/agenda/%s/datetimes/?min_places=' % agenda.slug)
assert not resp.json['data'][0]['disabled']
resp = app.get('/api/agenda/%s/datetimes/?min_places=' % agenda.slug, status=400)
resp = app.get('/api/agenda/%s/datetimes/?min_places=wrong' % agenda.slug, status=400)
assert resp.json['err'] == 1
@ -315,7 +315,10 @@ def test_datetimes_api_user_external_id(app):
status=400,
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'user_external_id and exclude_user_external_id have different values'
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['user_external_id'] == [
'user_external_id and exclude_user_external_id have different values'
]
def test_datetimes_api_hide_disabled(app):
@ -396,15 +399,19 @@ def test_agenda_api_date_range(app):
params = {'date_start': value}
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['date_start'] == [
'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'
]
for value in ['foo', '2017-05-42']:
params = {'date_end': value}
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['date_end'] == [
'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'
]
params = {'date_start': base_datetime.date().isoformat()}
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params=params)
@ -447,8 +454,10 @@ def test_agenda_api_date_range(app):
params = {'date_start': '2017-05-21 foo'}
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['date_start'] == [
'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'
]
for start in ['2017-05-30 09:00', '2017-05-30 09:00:00']:
params = {'date_start': start}
@ -467,8 +476,10 @@ def test_agenda_api_date_range(app):
params = {'date_end': '2017-06-01 foo'}
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['date_end'] == [
'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'
]
for end in ['2017-05-30 11:01', '2017-05-30 11:00:01']:
params = {'date_end': end}

View File

@ -1917,15 +1917,19 @@ def test_meetings_and_virtual_datetimes_date_filter(app):
params = {'date_start': value}
resp = app.get(foo_api_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'date_start format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['date_start'] == [
'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'
]
for value in ['foo', '2017-05-42']:
params = {'date_end': value}
resp = app.get(foo_api_url, params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'date_end format must be YYYY-MM-DD or YYYY-MM-DD HH:MM'
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['date_end'] == [
'Datetime has wrong format. Use one of these formats instead: YYYY-MM-DD, YYYY-MM-DD hh:mm, YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'
]
# exclude weekday1 through date_start, 4 slots each day * 5 days
params = {'date_start': (localtime(now()) + datetime.timedelta(days=2)).date().isoformat()}