api: add datetimes for multiple events agendas (#55370)

This commit is contained in:
Valentin Deniaud 2021-08-09 11:54:18 +02:00
parent 8f127f3606
commit e86d0cb11f
4 changed files with 234 additions and 4 deletions

View File

@ -12,6 +12,15 @@ class StringOrListField(serializers.ListField):
return super().to_internal_value(data)
class CommaSeparatedStringField(serializers.ListField):
def get_value(self, dictionary):
return super(serializers.ListField, self).get_value(dictionary)
def to_internal_value(self, data):
data = [s.strip() for s in data.split(',') if s.strip()]
return super().to_internal_value(data)
class SlotSerializer(serializers.Serializer):
label = serializers.CharField(max_length=250, allow_blank=True)
user_external_id = serializers.CharField(max_length=250, allow_blank=True)
@ -118,3 +127,9 @@ class DatetimesSerializer(DateRangeSerializer):
{'user_external_id': _('user_external_id and exclude_user_external_id have different values')}
)
return attrs
class MultipleAgendasDatetimesSerializer(DatetimesSerializer):
agendas = CommaSeparatedStringField(
required=True, child=serializers.SlugField(max_length=160, allow_blank=False)
)

View File

@ -20,6 +20,7 @@ from . import views
urlpatterns = [
url(r'^agenda/$', views.agendas),
url(r'^agendas/datetimes/$', views.agendas_datetimes, name='api-agendas-datetimes'),
url(r'^agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda_detail),
url(r'^agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
url(

View File

@ -449,11 +449,17 @@ def get_event_text(event, agenda, day=None):
def get_event_detail(
request, event, agenda=None, min_places=1, booked_user_external_id=None, show_events=None
request,
event,
agenda=None,
min_places=1,
booked_user_external_id=None,
show_events=None,
multiple_agendas=False,
):
agenda = agenda or event.agenda
details = {
'id': event.slug,
'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug,
'slug': event.slug, # kept for compatibility
'text': get_event_text(event, agenda),
'label': event.label or '',
@ -502,7 +508,9 @@ def get_event_detail(
return details
def get_events_meta_detail(request, events, agenda=None, min_places=1, show_events=None):
def get_events_meta_detail(
request, events, agenda=None, min_places=1, show_events=None, multiple_agendas=False
):
bookable_datetimes_number_total = 0
bookable_datetimes_number_available = 0
first_bookable_slot = None
@ -512,7 +520,12 @@ def get_events_meta_detail(request, events, agenda=None, min_places=1, show_even
bookable_datetimes_number_available += 1
if not first_bookable_slot:
first_bookable_slot = get_event_detail(
request, event, agenda=agenda, min_places=min_places, show_events=show_events
request,
event,
agenda=agenda,
min_places=min_places,
show_events=show_events,
multiple_agendas=multiple_agendas,
)
return {
'no_bookable_datetimes': bool(bookable_datetimes_number_available == 0),
@ -815,6 +828,74 @@ class Datetimes(APIView):
datetimes = Datetimes.as_view()
class MultipleAgendasDatetimes(APIView):
permission_classes = ()
serializer_class = serializers.MultipleAgendasDatetimesSerializer
def get(self, request):
serializer = self.serializer_class(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,
)
payload = serializer.validated_data
if 'events' in payload:
raise APIError(
_('events parameter is not supported'),
err_class='events parameter is not supported',
http_status=status.HTTP_400_BAD_REQUEST,
)
agenda_slugs = payload['agendas']
agendas = Agenda.objects.filter(slug__in=agenda_slugs, kind='events')
if not len(agendas) == len(agenda_slugs):
not_found_slugs = sorted(set(agenda_slugs) - {agenda.slug for agenda in agendas})
raise APIError(
_('events agendas do not exist: %s') % ', '.join(not_found_slugs),
err_class='events agendas do not exist',
http_status=status.HTTP_404_NOT_FOUND,
)
user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
entries = []
for agenda in agendas:
entries.extend(
agenda.get_open_events(
annotate_queryset=True,
min_start=payload.get('date_start'),
max_start=payload.get('date_end'),
user_external_id=user_external_id,
)
)
agenda_querystring_indexes = {agenda_slug: i for i, agenda_slug in enumerate(agenda_slugs)}
entries.sort(key=lambda event: (event.start_datetime, agenda_querystring_indexes[event.agenda.slug]))
response = {
'data': [
get_event_detail(
request,
x,
min_places=payload['min_places'],
booked_user_external_id=payload.get('user_external_id'),
multiple_agendas=True,
)
for x in entries
],
'meta': get_events_meta_detail(
request, entries, min_places=payload['min_places'], multiple_agendas=True
),
}
return Response(response)
agendas_datetimes = MultipleAgendasDatetimes.as_view()
class MeetingDatetimes(APIView):
permission_classes = ()

View File

@ -1357,3 +1357,136 @@ def test_recurring_events_api_list(app, freezer):
freezer.move_to(event.recurrence_end_date)
resp = app.get('/api/agenda/%s/recurring-events/' % agenda.slug)
assert len(resp.json['data']) == 1
@pytest.mark.freeze_time('2021-05-06 14:00')
def test_datetimes_multiple_agendas(app):
first_agenda = Agenda.objects.create(label='First agenda', kind='events')
Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
event = Event.objects.create(
slug='event',
start_datetime=now() + datetime.timedelta(days=5),
places=5,
agenda=first_agenda,
)
second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
event = Event.objects.create(
slug='event',
start_datetime=now() + datetime.timedelta(days=6),
places=5,
agenda=second_agenda,
)
Booking.objects.create(event=event)
agenda_slugs = '%s,%s' % (first_agenda.slug, second_agenda.slug)
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
assert len(resp.json['data']) == 2
assert resp.json['data'][0]['id'] == 'first-agenda@event'
assert resp.json['data'][0]['text'] == 'May 11, 2021, 4 p.m.'
assert resp.json['data'][0]['places']['available'] == 5
assert resp.json['data'][1]['id'] == 'second-agenda@event'
assert resp.json['data'][1]['text'] == 'May 12, 2021, 4 p.m.'
assert resp.json['data'][1]['places']['available'] == 4
# check user_external_id
Booking.objects.create(event=event, user_external_id='user')
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'user_external_id': 'user'})
assert resp.json['data'][0]['places']['available'] == 5
assert 'booked_for_external_user' not in resp.json['data'][0]
assert resp.json['data'][0]['disabled'] is False
assert resp.json['data'][1]['places']['available'] == 3
assert resp.json['data'][1]['booked_for_external_user'] == 'main-list'
assert resp.json['data'][1]['disabled'] is True
# check exclude_user_external_id
resp = app.get(
'/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'exclude_user_external_id': 'user'}
)
assert 'booked_for_external_user' not in resp.json['data'][0]
assert resp.json['data'][0]['disabled'] is False
assert 'booked_for_external_user' not in resp.json['data'][1]
assert resp.json['data'][1]['disabled'] is True
# check min_places
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4})
assert resp.json['data'][0]['disabled'] is False
assert resp.json['data'][1]['disabled'] is True
# check meta
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4})
assert resp.json['meta']['bookable_datetimes_number_total'] == 2
assert resp.json['meta']['bookable_datetimes_number_available'] == 1
assert resp.json['meta']['first_bookable_slot'] == resp.json['data'][0]
# check date_start
date_start = localtime() + datetime.timedelta(days=5, hours=1)
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'date_start': date_start})
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['id'] == 'second-agenda@event'
# check date_end
date_end = localtime() + datetime.timedelta(days=5, hours=1)
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'date_end': date_end})
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['id'] == 'first-agenda@event'
resp = app.get(
'/api/agendas/datetimes/',
params={'agendas': agenda_slugs, 'date_start': date_start, 'date_end': date_end},
)
assert len(resp.json['data']) == 0
# invalid slugs
resp = app.get('/api/agendas/datetimes/', params={'agendas': 'xxx'}, status=404)
assert resp.json['err_desc'] == 'events agendas do not exist: xxx'
resp = app.get('/api/agendas/datetimes/', params={'agendas': 'first-agenda,xxx,yyy'}, status=404)
assert resp.json['err_desc'] == 'events agendas do not exist: xxx, yyy'
# no support for past events
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs, 'events': 'past'}, status=400)
@pytest.mark.freeze_time('2021-05-06 14:00')
def test_datetimes_multiple_agendas_sort(app):
first_agenda = Agenda.objects.create(label='First agenda', kind='events')
Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
event = Event.objects.create(
label='10-05', start_datetime=now().replace(day=10), places=5, agenda=first_agenda
)
second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
event = Event.objects.create(
label='09-05', start_datetime=now().replace(day=9), places=5, agenda=second_agenda
)
third_agenda = Agenda.objects.create(label='Third agenda', kind='events')
Desk.objects.create(agenda=third_agenda, slug='_exceptions_holder')
event = Event.objects.create(
label='09-05', start_datetime=now().replace(day=9), places=5, agenda=third_agenda
)
# check events are ordered by start_datetime and then by agenda order in querystring
agenda_slugs = ','.join((first_agenda.slug, third_agenda.slug, second_agenda.slug))
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
assert len(resp.json['data']) == 3
assert resp.json['data'][0]['id'] == 'third-agenda@09-05'
assert resp.json['data'][1]['id'] == 'second-agenda@09-05'
assert resp.json['data'][2]['id'] == 'first-agenda@10-05'
@pytest.mark.freeze_time('2021-05-06 14:00')
def test_datetimes_multiple_agendas_queries(app):
for i in range(10):
agenda = Agenda.objects.create(label=str(i), kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda)
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agendas/datetimes/', params={'agendas': ','.join(str(i) for i in range(10))})
assert len(resp.json['data']) == 20
assert len(ctx.captured_queries) == 21