api: add datetimes for multiple events agendas (#55370)
This commit is contained in:
parent
8f127f3606
commit
e86d0cb11f
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 = ()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue