0001-api-add-datetimes-for-multiple-events-agendas-55370.patch
chrono/api/serializers.py | ||
---|---|---|
12 | 12 |
return super().to_internal_value(data) |
13 | 13 | |
14 | 14 | |
15 |
class CommaSeparatedStringField(serializers.ListField): |
|
16 |
def get_value(self, dictionary): |
|
17 |
return super(serializers.ListField, self).get_value(dictionary) |
|
18 | ||
19 |
def to_internal_value(self, data): |
|
20 |
data = [s.strip() for s in data.split(',') if s.strip()] |
|
21 |
return super().to_internal_value(data) |
|
22 | ||
23 | ||
15 | 24 |
class SlotSerializer(serializers.Serializer): |
16 | 25 |
label = serializers.CharField(max_length=250, allow_blank=True) |
17 | 26 |
user_external_id = serializers.CharField(max_length=250, allow_blank=True) |
... | ... | |
118 | 127 |
{'user_external_id': _('user_external_id and exclude_user_external_id have different values')} |
119 | 128 |
) |
120 | 129 |
return attrs |
130 | ||
131 | ||
132 |
class MultipleAgendasDatetimesSerializer(DatetimesSerializer): |
|
133 |
agendas = CommaSeparatedStringField( |
|
134 |
required=True, child=serializers.SlugField(max_length=160, allow_blank=False) |
|
135 |
) |
chrono/api/urls.py | ||
---|---|---|
20 | 20 | |
21 | 21 |
urlpatterns = [ |
22 | 22 |
url(r'^agenda/$', views.agendas), |
23 |
url(r'^agenda/datetimes/$', views.agendas_datetimes, name='api-agendas-datetimes'), |
|
23 | 24 |
url(r'^agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda_detail), |
24 | 25 |
url(r'^agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'), |
25 | 26 |
url( |
chrono/api/views.py | ||
---|---|---|
449 | 449 | |
450 | 450 | |
451 | 451 |
def get_event_detail( |
452 |
request, event, agenda=None, min_places=1, booked_user_external_id=None, show_events=None |
|
452 |
request, |
|
453 |
event, |
|
454 |
agenda=None, |
|
455 |
min_places=1, |
|
456 |
booked_user_external_id=None, |
|
457 |
show_events=None, |
|
458 |
multiple_agendas=False, |
|
453 | 459 |
): |
454 | 460 |
agenda = agenda or event.agenda |
455 | 461 |
details = { |
456 |
'id': event.slug, |
|
462 |
'id': '%s@%s' % (agenda.slug, event.slug) if multiple_agendas else event.slug,
|
|
457 | 463 |
'slug': event.slug, # kept for compatibility |
458 | 464 |
'text': get_event_text(event, agenda), |
459 | 465 |
'label': event.label or '', |
... | ... | |
502 | 508 |
return details |
503 | 509 | |
504 | 510 | |
505 |
def get_events_meta_detail(request, events, agenda=None, min_places=1, show_events=None): |
|
511 |
def get_events_meta_detail( |
|
512 |
request, events, agenda=None, min_places=1, show_events=None, multiple_agendas=False |
|
513 |
): |
|
506 | 514 |
bookable_datetimes_number_total = 0 |
507 | 515 |
bookable_datetimes_number_available = 0 |
508 | 516 |
first_bookable_slot = None |
... | ... | |
512 | 520 |
bookable_datetimes_number_available += 1 |
513 | 521 |
if not first_bookable_slot: |
514 | 522 |
first_bookable_slot = get_event_detail( |
515 |
request, event, agenda=agenda, min_places=min_places, show_events=show_events |
|
523 |
request, |
|
524 |
event, |
|
525 |
agenda=agenda, |
|
526 |
min_places=min_places, |
|
527 |
show_events=show_events, |
|
528 |
multiple_agendas=multiple_agendas, |
|
516 | 529 |
) |
517 | 530 |
return { |
518 | 531 |
'no_bookable_datetimes': bool(bookable_datetimes_number_available == 0), |
... | ... | |
815 | 828 |
datetimes = Datetimes.as_view() |
816 | 829 | |
817 | 830 | |
831 |
class MultipleAgendasDatetimes(APIView): |
|
832 |
permission_classes = () |
|
833 |
serializer_class = serializers.MultipleAgendasDatetimesSerializer |
|
834 | ||
835 |
def get(self, request): |
|
836 |
serializer = self.serializer_class(data=request.query_params) |
|
837 |
if not serializer.is_valid(): |
|
838 |
raise APIError( |
|
839 |
_('invalid payload'), |
|
840 |
err_class='invalid payload', |
|
841 |
errors=serializer.errors, |
|
842 |
http_status=status.HTTP_400_BAD_REQUEST, |
|
843 |
) |
|
844 |
payload = serializer.validated_data |
|
845 | ||
846 |
if 'events' in payload: |
|
847 |
raise APIError( |
|
848 |
_('events parameter is not supported'), |
|
849 |
err_class='events parameter is not supported', |
|
850 |
http_status=status.HTTP_400_BAD_REQUEST, |
|
851 |
) |
|
852 | ||
853 |
agenda_slugs = payload['agendas'] |
|
854 |
agendas = Agenda.objects.filter(slug__in=agenda_slugs, kind='events') |
|
855 |
if not len(agendas) == len(agenda_slugs): |
|
856 |
not_found_slugs = sorted(set(agenda_slugs) - {agenda.slug for agenda in agendas}) |
|
857 |
raise APIError( |
|
858 |
_('events agendas do not exist: %s') % ', '.join(not_found_slugs), |
|
859 |
err_class='events agendas do not exist', |
|
860 |
http_status=status.HTTP_404_NOT_FOUND, |
|
861 |
) |
|
862 | ||
863 |
user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id') |
|
864 |
entries = [] |
|
865 |
for agenda in agendas: |
|
866 |
entries.extend( |
|
867 |
agenda.get_open_events( |
|
868 |
annotate_queryset=True, |
|
869 |
min_start=payload.get('date_start'), |
|
870 |
max_start=payload.get('date_end'), |
|
871 |
user_external_id=user_external_id, |
|
872 |
) |
|
873 |
) |
|
874 | ||
875 |
response = { |
|
876 |
'data': [ |
|
877 |
get_event_detail( |
|
878 |
request, |
|
879 |
x, |
|
880 |
min_places=payload['min_places'], |
|
881 |
booked_user_external_id=payload.get('user_external_id'), |
|
882 |
multiple_agendas=True, |
|
883 |
) |
|
884 |
for x in entries |
|
885 |
], |
|
886 |
'meta': get_events_meta_detail( |
|
887 |
request, entries, min_places=payload['min_places'], multiple_agendas=True |
|
888 |
), |
|
889 |
} |
|
890 |
return Response(response) |
|
891 | ||
892 | ||
893 |
agendas_datetimes = MultipleAgendasDatetimes.as_view() |
|
894 | ||
895 | ||
818 | 896 |
class MeetingDatetimes(APIView): |
819 | 897 |
permission_classes = () |
820 | 898 |
tests/api/test_datetimes.py | ||
---|---|---|
2 | 2 |
import urllib.parse as urlparse |
3 | 3 | |
4 | 4 |
import pytest |
5 |
from django.db import connection |
|
5 | 6 |
from django.test import override_settings |
7 |
from django.test.utils import CaptureQueriesContext |
|
6 | 8 |
from django.utils.timezone import localtime, make_aware, now |
7 | 9 | |
8 | 10 |
from chrono.agendas.models import Agenda, Booking, Desk, Event, TimePeriodException |
... | ... | |
1344 | 1346 |
freezer.move_to(event.recurrence_end_date) |
1345 | 1347 |
resp = app.get('/api/agenda/%s/recurring-events/' % agenda.slug) |
1346 | 1348 |
assert len(resp.json['data']) == 1 |
1349 | ||
1350 | ||
1351 |
@pytest.mark.freeze_time('2021-05-06 14:00') |
|
1352 |
def test_datetimes_multiple_agendas(app): |
|
1353 |
first_agenda = Agenda.objects.create(label='First agenda', kind='events') |
|
1354 |
Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder') |
|
1355 |
event = Event.objects.create( |
|
1356 |
slug='event', |
|
1357 |
start_datetime=now() + datetime.timedelta(days=5), |
|
1358 |
places=5, |
|
1359 |
agenda=first_agenda, |
|
1360 |
) |
|
1361 |
second_agenda = Agenda.objects.create(label='Second agenda', kind='events') |
|
1362 |
Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder') |
|
1363 |
event = Event.objects.create( |
|
1364 |
slug='event', |
|
1365 |
start_datetime=now() + datetime.timedelta(days=6), |
|
1366 |
places=5, |
|
1367 |
agenda=second_agenda, |
|
1368 |
) |
|
1369 |
Booking.objects.create(event=event) |
|
1370 | ||
1371 |
agenda_slugs = '%s,%s' % (first_agenda.slug, second_agenda.slug) |
|
1372 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs}) |
|
1373 |
assert len(resp.json['data']) == 2 |
|
1374 |
assert resp.json['data'][0]['id'] == 'first-agenda@event' |
|
1375 |
assert resp.json['data'][0]['text'] == 'May 11, 2021, 4 p.m.' |
|
1376 |
assert resp.json['data'][0]['places']['available'] == 5 |
|
1377 | ||
1378 |
assert resp.json['data'][1]['id'] == 'second-agenda@event' |
|
1379 |
assert resp.json['data'][1]['text'] == 'May 12, 2021, 4 p.m.' |
|
1380 |
assert resp.json['data'][1]['places']['available'] == 4 |
|
1381 | ||
1382 |
# check user_external_id |
|
1383 |
Booking.objects.create(event=event, user_external_id='user') |
|
1384 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'user_external_id': 'user'}) |
|
1385 |
assert resp.json['data'][0]['places']['available'] == 5 |
|
1386 |
assert 'booked_for_external_user' not in resp.json['data'][0] |
|
1387 |
assert resp.json['data'][0]['disabled'] is False |
|
1388 | ||
1389 |
assert resp.json['data'][1]['places']['available'] == 3 |
|
1390 |
assert resp.json['data'][1]['booked_for_external_user'] == 'main-list' |
|
1391 |
assert resp.json['data'][1]['disabled'] is True |
|
1392 | ||
1393 |
# check exclude_user_external_id |
|
1394 |
resp = app.get( |
|
1395 |
'/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'exclude_user_external_id': 'user'} |
|
1396 |
) |
|
1397 |
assert 'booked_for_external_user' not in resp.json['data'][0] |
|
1398 |
assert resp.json['data'][0]['disabled'] is False |
|
1399 | ||
1400 |
assert 'booked_for_external_user' not in resp.json['data'][1] |
|
1401 |
assert resp.json['data'][1]['disabled'] is True |
|
1402 | ||
1403 |
# check min_places |
|
1404 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4}) |
|
1405 |
assert resp.json['data'][0]['disabled'] is False |
|
1406 |
assert resp.json['data'][1]['disabled'] is True |
|
1407 | ||
1408 |
# check meta |
|
1409 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'min_places': 4}) |
|
1410 |
assert resp.json['meta']['bookable_datetimes_number_total'] == 2 |
|
1411 |
assert resp.json['meta']['bookable_datetimes_number_available'] == 1 |
|
1412 |
assert resp.json['meta']['first_bookable_slot'] == resp.json['data'][0] |
|
1413 | ||
1414 |
# check date_start |
|
1415 |
date_start = localtime() + datetime.timedelta(days=5, hours=1) |
|
1416 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'date_start': date_start}) |
|
1417 |
assert len(resp.json['data']) == 1 |
|
1418 |
assert resp.json['data'][0]['id'] == 'second-agenda@event' |
|
1419 | ||
1420 |
# check date_end |
|
1421 |
date_end = localtime() + datetime.timedelta(days=5, hours=1) |
|
1422 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'date_end': date_end}) |
|
1423 |
assert len(resp.json['data']) == 1 |
|
1424 |
assert resp.json['data'][0]['id'] == 'first-agenda@event' |
|
1425 | ||
1426 |
resp = app.get( |
|
1427 |
'/api/agenda/datetimes/', |
|
1428 |
params={'agendas': agenda_slugs, 'date_start': date_start, 'date_end': date_end}, |
|
1429 |
) |
|
1430 |
assert len(resp.json['data']) == 0 |
|
1431 | ||
1432 |
# invalid slugs |
|
1433 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': 'xxx'}, status=404) |
|
1434 |
assert resp.json['err_desc'] == 'events agendas do not exist: xxx' |
|
1435 | ||
1436 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': 'first-agenda,xxx,yyy'}, status=404) |
|
1437 |
assert resp.json['err_desc'] == 'events agendas do not exist: xxx, yyy' |
|
1438 | ||
1439 |
# no support for past events |
|
1440 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': agenda_slugs, 'events': 'past'}, status=400) |
|
1441 | ||
1442 | ||
1443 |
@pytest.mark.freeze_time('2021-05-06 14:00') |
|
1444 |
def test_datetimes_multiple_agendas_queries(app): |
|
1445 |
for i in range(10): |
|
1446 |
agenda = Agenda.objects.create(label=str(i), kind='events') |
|
1447 |
Desk.objects.create(agenda=agenda, slug='_exceptions_holder') |
|
1448 |
Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda) |
|
1449 |
Event.objects.create(start_datetime=now() + datetime.timedelta(days=5), places=5, agenda=agenda) |
|
1450 | ||
1451 |
with CaptureQueriesContext(connection) as ctx: |
|
1452 |
resp = app.get('/api/agenda/datetimes/', params={'agendas': ','.join(str(i) for i in range(10))}) |
|
1453 |
assert len(resp.json['data']) == 20 |
|
1454 |
assert len(ctx.captured_queries) == 21 |
|
1347 |
- |