chrono/tests/api/test_fillslot.py

2804 lines
120 KiB
Python

import datetime
import urllib.parse as urlparse
from unittest import mock
import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.utils.timezone import localtime, now
from chrono.agendas.models import (
Agenda,
Booking,
BookingColor,
Desk,
Event,
MeetingType,
Resource,
TimePeriod,
VirtualMember,
)
pytestmark = pytest.mark.django_db
def test_booking_api(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
# unauthenticated
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id), status=401)
for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy)
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.slug][0]['api'][
'fillslot_url'
]
assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (
agenda.slug,
event.slug,
)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id))
Booking.objects.get(id=resp.json['booking_id'])
assert resp.json['datetime'] == localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
assert 'booking_url' in resp.json['api']
assert 'accept_url' in resp.json['api']
assert 'suspend_url' in resp.json['api']
assert 'cancel_url' in resp.json['api']
assert 'ics_url' in resp.json['api']
assert 'anonymize_url' in resp.json['api']
assert urlparse.urlparse(resp.json['api']['booking_url']).netloc
assert urlparse.urlparse(resp.json['api']['accept_url']).netloc
assert urlparse.urlparse(resp.json['api']['suspend_url']).netloc
assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
assert urlparse.urlparse(resp.json['api']['ics_url']).netloc
assert urlparse.urlparse(resp.json['api']['anonymize_url']).netloc
assert Booking.objects.count() == 1
# access by slug
event.slug = 'bar'
event.save()
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.slug))
assert Booking.objects.count() == 2
assert Booking.objects.filter(event__agenda=agenda).count() == 2
# test with additional data
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
params={
'label': 'foo',
'user_name': 'bar',
'backoffice_url': 'http://example.net/',
'cancel_callback_url': 'http://example.net/jump/trigger/',
'user_email': 'bar@bar.com',
'user_phone_number': '+33123456789',
'form_url': 'http://example.net/',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.label == 'foo'
assert booking.user_first_name == ''
assert booking.user_last_name == 'bar'
assert booking.backoffice_url == 'http://example.net/'
assert booking.cancel_callback_url == 'http://example.net/jump/trigger/'
assert booking.user_email == 'bar@bar.com'
assert booking.user_phone_number == '+33123456789'
assert booking.form_url == 'http://example.net/'
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
params={
'label': 'foo',
'user_first_name': 'foo',
'user_last_name': 'bar',
'backoffice_url': 'http://example.net/',
'cancel_callback_url': 'http://example.net/jump/trigger/',
'user_email': 'bar@bar.com',
'user_phone_number': '+33123456789',
'form_url': 'http://example.net/',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.label == 'foo'
assert booking.user_first_name == 'foo'
assert booking.user_last_name == 'bar'
assert booking.backoffice_url == 'http://example.net/'
assert booking.cancel_callback_url == 'http://example.net/jump/trigger/'
assert booking.user_email == 'bar@bar.com'
assert booking.user_phone_number == '+33123456789'
assert booking.form_url == 'http://example.net/'
# blank data are OK
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
params={'label': '', 'user_first_name': '', 'user_last_name': '', 'backoffice_url': ''},
)
assert Booking.objects.get(id=resp.json['booking_id']).label == ''
assert Booking.objects.get(id=resp.json['booking_id']).user_first_name == ''
assert Booking.objects.get(id=resp.json['booking_id']).user_last_name == ''
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == ''
# extra data stored in extra_data field
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
params={'label': 'l', 'user_last_name': 'u', 'backoffice_url': '', 'foo': 'bar'},
)
assert Booking.objects.get(id=resp.json['booking_id']).label == 'l'
assert Booking.objects.get(id=resp.json['booking_id']).user_last_name == 'u'
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == ''
assert Booking.objects.get(id=resp.json['booking_id']).extra_data == {'foo': 'bar'}
# anonymize
booking_id = resp.json['booking_id']
resp = app.post(resp.json['api']['anonymize_url'])
assert Booking.objects.get(id=booking_id).anonymization_datetime is not None
# test invalid data are refused
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
params={'user_last_name': {'foo': 'bar'}},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid payload' # legacy
assert resp.json['err_class'] == 'invalid payload'
assert resp.json['err_desc'] == 'invalid payload'
assert len(resp.json['errors']) == 1
assert 'user_last_name' in resp.json['errors']
# test parameters wrongly passed in query are refused
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/?backoffice_url=https://example.com&label=test' % (agenda.id, event.id),
status=400,
)
assert resp.json['err'] == 1
assert (
resp.json['err_class']
== 'parameters "backoffice_url, label" must be included in request body, not query'
)
assert (
resp.json['err_desc']
== 'parameters "backoffice_url, label" must be included in request body, not query'
)
resp = app.post('/api/agenda/foobar/fillslot/%s/' % event.id, status=404)
resp = app.post('/api/agenda/0/fillslot/%s/' % event.id, status=404)
def test_booking_api_check_delays(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
)
event = Event.objects.create(
slug='event-slug',
start_datetime=localtime() + datetime.timedelta(days=5),
places=5,
agenda=agenda,
)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
assert resp.json['err'] == 0
# test minimal_booking_delay
agenda.minimal_booking_delay = 6
agenda.save()
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'bypass_delays': True})
assert resp.json['err'] == 0
# test maximal_booking_delay
agenda.minimal_booking_delay = 0
agenda.maximal_booking_delay = 3
agenda.save()
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'bypass_delays': True})
assert resp.json['err'] == 0
@pytest.mark.freeze_time('2021-02-23')
def test_booking_api_exclude_slots(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
)
event = Event.objects.create(
slug='event-slug',
start_datetime=localtime().replace(hour=10, minute=0),
places=5,
agenda=agenda,
)
Booking.objects.create(event=event, user_external_id='42')
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'user_external_id': '42'}
)
assert resp.json['err'] == 0
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
params={'user_external_id': '42', 'exclude_user': True},
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event is already booked by user'
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'exclude_user': True})
assert resp.json['err'] == 0
event.delete()
# recurrent event
start_datetime = localtime().replace(hour=12, minute=0)
event = Event.objects.create(
slug='recurrent',
start_datetime=start_datetime,
recurrence_days=[start_datetime.weekday()],
places=3,
agenda=agenda,
)
first_recurrence = event.get_or_create_event_recurrence(event.start_datetime)
Booking.objects.create(event=first_recurrence, user_external_id='42')
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, first_recurrence.slug),
params={'user_external_id': '42'},
)
assert resp.json['err'] == 0
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, first_recurrence.slug),
params={'user_external_id': '42', 'exclude_user': True},
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event is already booked by user'
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, first_recurrence.slug), params={'exclude_user': True}
)
assert resp.json['err'] == 0
@pytest.mark.freeze_time('2021-02-25')
def test_booking_api_meetings_agenda_exclude_slots(app, user):
tomorrow = now() + datetime.timedelta(days=1)
agenda = Agenda.objects.create(
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
)
desk = Desk.objects.create(agenda=agenda, slug='desk')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
TimePeriod.objects.create(
weekday=tomorrow.date().weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
desk.duplicate()
desk.duplicate()
event = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=9, minute=0),
desk=desk,
)
Booking.objects.create(event=event, user_external_id='42')
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post(
'/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (agenda.slug, meeting_type.slug),
params={'user_external_id': '42'},
)
assert resp.json['err'] == 0
resp = app.post(
'/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (agenda.slug, meeting_type.slug),
params={'user_external_id': '42', 'exclude_user': True},
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'no more desk available'
resp = app.post(
'/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (agenda.slug, meeting_type.slug),
params={'exclude_user': True},
)
assert resp.json['err'] == 0
def test_booking_api_fillslots(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
events_slugs = [x.slug for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
assert len(events_ids) == 3
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] # first event
# unauthenticated
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, status=401)
for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy)
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
api_event_slugs = [x['id'] for x in resp_datetimes.json['data']]
assert api_event_slugs == events_slugs
assert Booking.objects.count() == 0
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_ids})
primary_booking_id = resp.json['booking_id']
Booking.objects.get(id=primary_booking_id)
assert resp.json['datetime'] == localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
assert 'booking_url' in resp.json['api']
assert 'accept_url' in resp.json['api']
assert 'suspend_url' in resp.json['api']
assert 'cancel_url' in resp.json['api']
assert urlparse.urlparse(resp.json['api']['booking_url']).netloc
assert urlparse.urlparse(resp.json['api']['accept_url']).netloc
assert urlparse.urlparse(resp.json['api']['suspend_url']).netloc
assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
assert Booking.objects.count() == 3
# these 3 bookings are related, the first is the primary one
bookings = Booking.objects.all().order_by('pk')
assert bookings[0].primary_booking is None
assert bookings[1].primary_booking.id == bookings[0].id == primary_booking_id
assert bookings[2].primary_booking.id == bookings[0].id == primary_booking_id
# access by slug
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_slugs})
primary_booking_id_2 = resp.json['booking_id']
assert Booking.objects.count() == 6
assert Booking.objects.filter(event__agenda=agenda).count() == 6
# 6 = 2 primary + 2*2 secondary
assert Booking.objects.filter(event__agenda=agenda, primary_booking__isnull=True).count() == 2
assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id).count() == 2
assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id_2).count() == 2
# test with additional data
resp = app.post_json(
'/api/agenda/%s/fillslots/' % agenda.id,
params={
'slots': events_ids,
'label': 'foo',
'user_external_id': 'some_external_id',
'user_last_name': 'bar',
'user_display_label': 'foo',
'backoffice_url': 'http://example.net/',
},
)
booking_id = resp.json['booking_id']
booking = Booking.objects.get(pk=booking_id)
assert booking.label == 'foo'
assert booking.user_external_id == 'some_external_id'
assert booking.user_last_name == 'bar'
assert booking.user_display_label == 'foo'
assert booking.backoffice_url == 'http://example.net/'
assert Booking.objects.filter(primary_booking=booking_id, label='foo').count() == 2
# cancel
cancel_url = resp.json['api']['cancel_url']
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
assert Booking.objects.get(id=booking_id).cancellation_datetime is None
resp_cancel = app.post(cancel_url)
assert resp_cancel.json['err'] == 0
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 3
assert Booking.objects.get(id=booking_id).cancellation_datetime is not None
# extra data stored in extra_data field
resp = app.post_json(
'/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids, 'label': 'l', 'user_last_name': 'u', 'backoffice_url': '', 'foo': 'bar'},
)
assert Booking.objects.get(id=resp.json['booking_id']).label == 'l'
assert Booking.objects.get(id=resp.json['booking_id']).user_last_name == 'u'
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == ''
assert Booking.objects.get(id=resp.json['booking_id']).extra_data == {'foo': 'bar'}
for booking in Booking.objects.filter(primary_booking=resp.json['booking_id']):
assert booking.extra_data == {'foo': 'bar'}
# test invalid data are refused
resp = app.post_json(
'/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids, 'user_last_name': {'foo': 'bar'}},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid payload' # legacy
assert resp.json['err_class'] == 'invalid payload'
assert resp.json['err_desc'] == 'invalid payload'
assert len(resp.json['errors']) == 1
assert 'user_last_name' in resp.json['errors']
# empty or missing slots
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': []}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid payload' # legacy
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['slots'] == ['This field is required.']
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid payload' # legacy
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['slots'] == ['This field is required.']
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': ''}, status=400)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['slots'] == ['This field is required.']
# invalid slots format
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': 'foobar'}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid slugs: foobar' # legacy
assert resp.json['err_class'] == 'invalid slugs: foobar'
assert resp.json['err_desc'] == 'invalid slugs: foobar'
# unknown agendas
resp = app.post('/api/agenda/foobar/fillslots/', status=404)
resp = app.post('/api/agenda/0/fillslots/', status=404)
# check bookable period
with mock.patch('chrono.agendas.models.Event.in_bookable_period') as in_bookable_period:
in_bookable_period.return_value = True
resp = app.post_json(
'/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids},
)
assert resp.json['err'] == 0
in_bookable_period.return_value = False
resp = app.post_json(
'/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids},
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'event not bookable' # legacy
assert resp.json['err_class'] == 'event not bookable'
assert resp.json['err_desc'] == 'event foo-bar-event is not bookable'
def test_booking_api_fillslots_slots_string_param(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]
assert len(events_ids) == 3
app.authorization = ('Basic', ('john.doe', 'password'))
# empty string
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': ''}, status=400)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'invalid payload'
assert resp.json['err_desc'] == 'invalid payload'
slots_string_param = ','.join([str(e) for e in events_ids])
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots_string_param})
primary_booking_id = resp.json['booking_id']
Booking.objects.get(id=primary_booking_id)
assert Booking.objects.count() == 3
start = now() + datetime.timedelta(days=2)
Event.objects.create(label='Long Slug', slug='a' * 100, start_datetime=start, places=2, agenda=agenda)
Event.objects.create(label='Long Slug', slug='b' * 100, start_datetime=start, places=2, agenda=agenda)
events_ids = [x.id for x in Event.objects.filter(label='Long Slug')]
slots_string_param = ','.join([str(e) for e in events_ids])
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots_string_param})
assert Booking.objects.count() == 5
def test_booking_api_meeting(app, meetings_agenda, user):
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
event_id = resp.json['data'][2]['id']
assert urlparse.urlparse(
resp.json['data'][2]['api']['fillslot_url']
).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
app.authorization = ('Basic', ('john.doe', 'password'))
# verify malformed event_pk returns a 400
resp_booking = app.post('/api/agenda/%s/fillslot/None/' % agenda_id, status=400)
assert resp_booking.json['err'] == 1
# make a booking
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert Booking.objects.count() == 1
assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
)
assert resp_booking.json['end_datetime'] == localtime(
Booking.objects.all()[0].event.end_datetime
).strftime('%Y-%m-%d %H:%M:%S')
assert resp_booking.json['duration'] == 30
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 1
# try booking the same timeslot
resp2 = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert resp2.json['err'] == 1
assert resp2.json['reason'] == 'no more desk available' # legacy
assert resp2.json['err_class'] == 'no more desk available'
assert resp2.json['err_desc'] == 'no more desk available'
# try booking another timeslot
event_id = resp.json['data'][3]['id']
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert resp.json['err'] == 0
assert Booking.objects.count() == 2
def test_booking_api_meeting_colors(app, user):
agenda = Agenda.objects.create(
label='foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56
)
meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
default_desk = Desk.objects.create(agenda=agenda, label='Desk 1')
TimePeriod.objects.create(
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk
)
datetimes_resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
event_id = datetimes_resp.json['data'][2]['id']
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id),
params={
'use_color_for': 'Cooking',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.color.label == 'Cooking'
assert booking.color.index == 0
event_id = datetimes_resp.json['data'][3]['id']
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id),
params={
'use_color_for': 'Cooking',
},
)
new_booking = Booking.objects.get(id=resp.json['booking_id'])
assert new_booking.color.index == 0
event_id = datetimes_resp.json['data'][4]['id']
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id),
params={
'use_color_for': 'Swimming',
},
)
new_booking = Booking.objects.get(id=resp.json['booking_id'])
assert new_booking.color.label == 'Swimming'
assert new_booking.color.index == 1
for i in range((BookingColor.COLOR_COUNT * 2) - 2):
event_id = datetimes_resp.json['data'][i]['id']
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id),
params={
'use_color_for': str(i),
},
)
assert BookingColor.objects.count() == BookingColor.COLOR_COUNT * 2
assert BookingColor.objects.distinct('index').order_by().count() == BookingColor.COLOR_COUNT
def test_booking_api_meeting_with_resources(app, user):
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
tomorrow_str = tomorrow.isoformat()
agenda = Agenda.objects.create(
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
)
resource1 = Resource.objects.create(label='Resource 1')
resource2 = Resource.objects.create(label='Resource 2')
resource3 = Resource.objects.create(label='Resource 3')
agenda.resources.add(resource1, resource2, resource3)
desk = Desk.objects.create(agenda=agenda, slug='desk')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
TimePeriod.objects.create(
weekday=tomorrow.weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
app.authorization = ('Basic', ('john.doe', 'password'))
# make a booking without resource
resp = app.post('/api/agenda/%s/fillslot/%s:%s-1000/' % (agenda.pk, meeting_type.pk, tomorrow_str))
assert resp.json['datetime'] == '%s 10:00:00' % tomorrow_str
assert resp.json['end_datetime'] == '%s 10:30:00' % tomorrow_str
assert resp.json['duration'] == 30
assert resp.json['resources'] == []
booking = Booking.objects.latest('pk')
assert list(booking.event.resources.all()) == []
# now try to book also a resource - slot not free
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-1000/?resources=%s'
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug)
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available' # legacy
assert resp.json['err_class'] == 'no more desk available'
assert resp.json['err_desc'] == 'no more desk available'
# slot is free
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s'
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug)
)
assert resp.json['datetime'] == '%s 09:00:00' % tomorrow_str
assert resp.json['end_datetime'] == '%s 09:30:00' % tomorrow_str
assert resp.json['duration'] == 30
assert resp.json['resources'] == [resource1.slug]
booking = Booking.objects.latest('pk')
assert list(booking.event.resources.all()) == [resource1]
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-0930/?resources=%s,%s'
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug, resource2.slug)
)
assert resp.json['datetime'] == '%s 09:30:00' % tomorrow_str
assert resp.json['end_datetime'] == '%s 10:00:00' % tomorrow_str
assert resp.json['duration'] == 30
assert resp.json['resources'] == [resource1.slug, resource2.slug]
booking = Booking.objects.latest('pk')
assert list(booking.event.resources.all()) == [resource1, resource2]
# resource is unknown or not valid for this agenda
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=foobarblah'
% (agenda.pk, meeting_type.pk, tomorrow_str),
status=400,
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid slugs: foobarblah' # legacy
assert resp.json['err_class'] == 'invalid slugs: foobarblah'
assert resp.json['err_desc'] == 'invalid slugs: foobarblah'
agenda.resources.remove(resource3)
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s'
% (agenda.pk, meeting_type.pk, tomorrow_str, resource3.slug),
status=400,
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid slugs: resource-3' # legacy
assert resp.json['err_class'] == 'invalid slugs: resource-3'
assert resp.json['err_desc'] == 'invalid slugs: resource-3'
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s,foobarblah'
% (agenda.pk, meeting_type.pk, tomorrow_str, resource3.slug),
status=400,
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid slugs: foobarblah, resource-3' # legacy
assert resp.json['err_class'] == 'invalid slugs: foobarblah, resource-3'
assert resp.json['err_desc'] == 'invalid slugs: foobarblah, resource-3'
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s,foobarblah'
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug),
status=400,
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid slugs: foobarblah' # legacy
assert resp.json['err_class'] == 'invalid slugs: foobarblah'
assert resp.json['err_desc'] == 'invalid slugs: foobarblah'
# booking is canceled: slot is free
booking.cancel()
resp = app.post(
'/api/agenda/%s/fillslot/%s:%s-0930/?resources=%s,%s'
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug, resource2.slug)
)
assert resp.json['datetime'] == '%s 09:30:00' % tomorrow_str
assert resp.json['end_datetime'] == '%s 10:00:00' % tomorrow_str
assert resp.json['duration'] == 30
assert resp.json['resources'] == [resource1.slug, resource2.slug]
booking = Booking.objects.latest('pk')
assert list(booking.event.resources.all()) == [resource1, resource2]
def test_booking_api_meeting_fillslots(app, meetings_agenda, user):
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
app.authorization = ('Basic', ('john.doe', 'password'))
resp_booking = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
assert Booking.objects.count() == 2
primary_booking = Booking.objects.filter(primary_booking__isnull=True).first()
secondary_booking = Booking.objects.filter(primary_booking=primary_booking.id).first()
assert resp_booking.json['datetime'] == localtime(primary_booking.event.start_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
)
assert resp_booking.json['end_datetime'] == localtime(secondary_booking.event.end_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
)
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 2
# try booking the same timeslots
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
assert resp2.json['err'] == 1
assert resp2.json['reason'] == 'no more desk available' # legacy
assert resp2.json['err_class'] == 'no more desk available'
assert resp2.json['err_desc'] == 'no more desk available'
# try booking partially free timeslots (one free, one busy)
nonfree_slots = [resp.json['data'][0]['id'], resp.json['data'][2]['id']]
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': nonfree_slots})
assert resp2.json['err'] == 1
assert resp2.json['reason'] == 'no more desk available' # legacy
assert resp2.json['err_class'] == 'no more desk available'
assert resp2.json['err_desc'] == 'no more desk available'
# booking other free timeslots
free_slots = [resp.json['data'][3]['id'], resp.json['data'][2]['id']]
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': free_slots})
assert resp2.json['err'] == 0
cancel_url = resp2.json['api']['cancel_url']
assert Booking.objects.count() == 4
# 4 = 2 primary + 2 secondary
assert Booking.objects.filter(primary_booking__isnull=True).count() == 2
assert Booking.objects.filter(primary_booking__isnull=False).count() == 2
# cancel
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
resp_cancel = app.post(cancel_url)
assert resp_cancel.json['err'] == 0
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 2
def test_booking_api_meeting_fillslots_wrong_slot(app, user):
agenda = Agenda.objects.create(label='Foo', kind='meetings')
app.authorization = ('Basic', ('john.doe', 'password'))
impossible_slots = ['1:2017-05-22-1130', '2:2017-05-22-1100']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': impossible_slots}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'all slots must have the same meeting type id (1)' # legacy
assert resp.json['err_class'] == 'all slots must have the same meeting type id (1)'
assert resp.json['err_desc'] == 'all slots must have the same meeting type id (1)'
unknown_slots = ['0:2017-05-22-1130']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': unknown_slots}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid meeting type id: 0' # legacy
assert resp.json['err_class'] == 'invalid meeting type id: 0'
assert resp.json['err_desc'] == 'invalid meeting type id: 0'
unknown_slots = ['foobar:2017-05-22-1130']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': unknown_slots}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'invalid meeting type id: foobar' # legacy
assert resp.json['err_class'] == 'invalid meeting type id: foobar'
assert resp.json['err_desc'] == 'invalid meeting type id: foobar'
badformat_slots = ['foo:2020-10-28-14h00']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': badformat_slots}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'bad datetime format: 2020-10-28-14h00' # legacy
assert resp.json['err_class'] == 'bad datetime format: 2020-10-28-14h00'
assert resp.json['err_desc'] == 'bad datetime format: 2020-10-28-14h00'
def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user):
meetings_agenda.maximal_booking_delay = 365
meetings_agenda.save()
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
event_index = 26 * 18
event_id = resp.json['data'][event_index]['id']
assert event_id[-4:] == resp.json['data'][2 * 18]['id'][-4:]
assert urlparse.urlparse(
resp.json['data'][event_index]['api']['fillslot_url']
).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
app.authorization = ('Basic', ('john.doe', 'password'))
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert Booking.objects.count() == 1
assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert resp.json['data'][event_index]['disabled']
def test_booking_api_with_data(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
app.authorization = ('Basic', ('john.doe', 'password'))
app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), params={'hello': 'world'})
assert Booking.objects.count() == 1
assert Booking.objects.all()[0].extra_data == {'hello': 'world'}
def test_booking_api_available(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0)
for i in range(0, 10):
event = Event.objects.create(
slug='event-slug%i' % i,
start_datetime=(now() + datetime.timedelta(days=5)).replace(hour=10, minute=i),
places=20,
agenda=agenda,
)
app.authorization = ('Basic', ('john.doe', 'password'))
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert len(ctx.captured_queries) == 4
assert resp.json['data'][-1]['places']['total'] == 20
assert resp.json['data'][-1]['places']['available'] == 20
assert resp.json['data'][-1]['places']['reserved'] == 0
assert resp.json['data'][-1]['places']['full'] is False
assert 'waiting_list_total' not in resp.json['data'][-1]['places']
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
assert resp.json['err'] == 0
assert resp.json['places']['total'] == 20
assert resp.json['places']['available'] == 19
assert resp.json['places']['reserved'] == 1
assert resp.json['places']['full'] is False
assert 'waiting_list_total' not in resp.json['places']
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert resp.json['data'][-1]['places']['total'] == 20
assert resp.json['data'][-1]['places']['available'] == 19
assert resp.json['data'][-1]['places']['reserved'] == 1
assert resp.json['data'][-1]['places']['full'] is False
assert 'waiting_list_total' not in resp.json['data'][-1]['places']
Booking.objects.create(event=event, in_waiting_list=True)
event.waiting_list_places = 5
event.save()
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
assert resp.json['err'] == 0
assert resp.json['places']['total'] == 20
assert resp.json['places']['available'] == 19
assert resp.json['places']['reserved'] == 1
assert resp.json['places']['full'] is False
assert resp.json['places']['waiting_list_total'] == 5
assert resp.json['places']['waiting_list_available'] == 3
assert resp.json['places']['waiting_list_reserved'] == 2
assert resp.json['places']['waiting_list_activated'] is True
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
assert resp.json['data'][-1]['places']['total'] == 20
assert resp.json['data'][-1]['places']['available'] == 19
assert resp.json['data'][-1]['places']['reserved'] == 1
assert resp.json['data'][-1]['places']['full'] is False
assert resp.json['data'][-1]['places']['waiting_list_total'] == 5
assert resp.json['data'][-1]['places']['waiting_list_available'] == 3
assert resp.json['data'][-1]['places']['waiting_list_reserved'] == 2
assert resp.json['data'][-1]['places']['waiting_list_activated'] is True
# not for mettings agenda
meetings_agenda = Agenda.objects.create(label='meetings', kind='meetings')
desk = Desk.objects.create(label='foo', agenda=meetings_agenda)
meeting_type = MeetingType.objects.create(agenda=meetings_agenda, label='Blah', duration=30)
TimePeriod.objects.create(
weekday=3, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk
)
resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (meetings_agenda.slug, meeting_type.slug))
assert 'places' not in resp.json['data'][0]
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.pk, resp.json['data'][0]['id']))
assert resp.json['err'] == 0
assert 'places' not in resp.json
# not for multiple booking
events = [
x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period()
][:2]
slots = [x.pk for x in events]
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '3'})
assert resp.json['err'] == 0
assert 'places' not in resp.json
def test_booking_api_force_waiting_list(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
app.authorization = ('Basic', ('john.doe', 'password'))
# no waiting list
assert event.waiting_list_places == 0
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk), params={'force_waiting_list': True}
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no waiting list' # legacy
assert resp.json['err_class'] == 'no waiting list'
assert resp.json['err_desc'] == 'no waiting list'
event.waiting_list_places = 2
event.save()
# add a booking
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
assert resp.json['err'] == 0
assert resp.json['places']['total'] == 20
assert resp.json['places']['available'] == 19
assert resp.json['places']['reserved'] == 1
assert resp.json['places']['full'] is False
assert resp.json['places']['waiting_list_total'] == 2
assert resp.json['places']['waiting_list_available'] == 2
assert resp.json['places']['waiting_list_reserved'] == 0
assert resp.json['places']['waiting_list_activated'] is False
# add another booking
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk), params={'force_waiting_list': False}
)
assert resp.json['err'] == 0
assert resp.json['places']['total'] == 20
assert resp.json['places']['available'] == 18
assert resp.json['places']['reserved'] == 2
assert resp.json['places']['full'] is False
assert resp.json['places']['waiting_list_total'] == 2
assert resp.json['places']['waiting_list_available'] == 2
assert resp.json['places']['waiting_list_reserved'] == 0
assert resp.json['places']['waiting_list_activated'] is False
# add a booking, but in waiting list
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk), params={'force_waiting_list': True}
)
assert resp.json['err'] == 0
assert resp.json['places']['total'] == 20
assert resp.json['places']['available'] == 18
assert resp.json['places']['reserved'] == 2
assert resp.json['places']['full'] is False
assert resp.json['places']['waiting_list_total'] == 2
assert resp.json['places']['waiting_list_available'] == 1
assert resp.json['places']['waiting_list_reserved'] == 1
assert resp.json['places']['waiting_list_activated'] is True
# add a booking => booked in waiting list
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk))
assert resp.json['err'] == 0
assert resp.json['places']['total'] == 20
assert resp.json['places']['available'] == 18
assert resp.json['places']['reserved'] == 2
assert resp.json['places']['full'] is True
assert resp.json['places']['waiting_list_total'] == 2
assert resp.json['places']['waiting_list_available'] == 0
assert resp.json['places']['waiting_list_reserved'] == 2
assert resp.json['places']['waiting_list_activated'] is True
# waiting list is full
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk), params={'force_waiting_list': True}
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
def test_booking_api_with_cancel_booking(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
event_0, event_1, event_2 = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][
0:4
]
app.authorization = ('Basic', ('john.doe', 'password'))
app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id))
assert Booking.objects.count() == 1
first_booking = Booking.objects.first()
# Book a new event and cancel previous booking
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id),
params={'cancel_booking_id': first_booking.pk},
)
assert resp.json['err'] == 0
assert resp.json['cancelled_booking_id'] == first_booking.pk
assert Booking.objects.count() == 2
first_booking = Booking.objects.get(pk=first_booking.pk)
assert first_booking.cancellation_datetime
# Cancelling an already cancelled booking returns an error
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id),
params={'cancel_booking_id': first_booking.pk},
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'cancel booking: booking already cancelled' # legacy
assert resp.json['err_class'] == 'cancel booking: booking already cancelled'
assert resp.json['err_desc'] == 'cancel booking: booking already cancelled'
assert Booking.objects.count() == 2
# Cancelling a non existent booking returns an error
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id), params={'cancel_booking_id': '-1'}
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'cancel booking: booking does no exist' # legacy
assert resp.json['err_class'] == 'cancel booking: booking does no exist'
assert resp.json['err_desc'] == 'cancel booking: booking does no exist'
assert Booking.objects.count() == 2
# Cancelling booking with different count than new booking
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event_2.id), params={'count': 2})
assert resp.json['err'] == 0
assert Booking.objects.count() == 4
booking_id = resp.json['booking_id']
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event_1.id),
params={'cancel_booking_id': booking_id, 'count': 1},
)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'cancel booking: count is different' # legacy
assert resp.json['err_class'] == 'cancel booking: count is different'
assert resp.json['err_desc'] == 'cancel booking: count is different'
assert Booking.objects.count() == 4
# cancel_booking_id must be an integer
app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id),
params={'cancel_booking_id': 'no an integer'},
status=400,
)
# cancel_booking_id can be empty or null
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id), params={'cancel_booking_id': ''}
)
assert resp.json['err'] == 0
assert 'cancelled_booking_id' not in resp.json
assert Booking.objects.count() == 5
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event_0.id), params={'cancel_booking_id': None}
)
assert resp.json['err'] == 0
assert 'cancelled_booking_id' not in resp.json
assert Booking.objects.count() == 6
def test_soldout(app, some_data, user):
agenda_id = Agenda.objects.filter(label='Foo bar')[0].id
event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0]
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id)
assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 3
assert event.slug in [x['id'] for x in resp.json['data']]
for _ in range(event.places):
Booking(event=event).save()
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id)
assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 2
assert event.slug not in [x['id'] for x in resp.json['data'] if not x.get('disabled')]
assert event.slug in [x['id'] for x in resp.json['data'] if x.get('disabled')]
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
def test_waiting_list_booking(app, some_data, user):
agenda_id = Agenda.objects.filter(label='Foo bar')[0].id
event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0]
event.waiting_list_places = 5
event.save()
for _ in range(event.places):
Booking(event=event).save()
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
assert resp.json['err'] == 0
assert resp.json['in_waiting_list'] is True
assert 'accept_url' in resp.json['api']
assert 'suspend_url' in resp.json['api']
assert 'cancel_url' in resp.json['api']
assert 'ics_url' in resp.json['api']
assert urlparse.urlparse(resp.json['api']['accept_url']).netloc
assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
assert urlparse.urlparse(resp.json['api']['ics_url']).netloc
# cancel a booking that was not on the waiting list
booking = Booking.objects.filter(event=event, in_waiting_list=False)[0]
booking.cancel()
# check new booking is still done on the waiting list
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
assert resp.json['err'] == 0
assert resp.json['in_waiting_list'] is True
# fill the waiting list
for _ in range(event.waiting_list_places):
Booking(event=event, in_waiting_list=True).save()
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
def test_multiple_booking_api(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/?count=NaN' % (agenda.slug, event.id), status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == "invalid value for count (NaN)" # legacy
assert resp.json['err_class'] == "invalid value for count (NaN)"
assert resp.json['err_desc'] == "invalid value for count (NaN)"
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/?count=0' % (agenda.slug, event.id), status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == "count cannot be less than or equal to zero" # legacy
assert resp.json['err_class'] == "count cannot be less than or equal to zero"
assert resp.json['err_desc'] == "count cannot be less than or equal to zero"
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/?count=-3' % (agenda.slug, event.id), status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == "count cannot be less than or equal to zero" # legacy
assert resp.json['err_class'] == "count cannot be less than or equal to zero"
assert resp.json['err_desc'] == "count cannot be less than or equal to zero"
resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
Booking.objects.get(id=resp.json['booking_id'])
assert resp.json['datetime'] == localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
assert 'accept_url' in resp.json['api']
assert 'cancel_url' in resp.json['api']
assert Event.objects.get(id=event.id).booked_places == 3
app.post('/api/agenda/%s/fillslot/%s/?count=2' % (agenda.slug, event.id))
assert Event.objects.get(id=event.id).booked_places == 5
resp = app.post(resp.json['api']['cancel_url'])
assert Event.objects.get(id=event.id).booked_places == 2
# check available places overflow
event.places = 3
event.waiting_list_places = 8
event.save()
resp3 = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
# check waiting list overflow
resp = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
# accept the waiting list
resp = app.post(resp3.json['api']['accept_url'])
assert Event.objects.get(id=event.id).booked_places == 7
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
# check with a short waiting list
Booking.objects.all().delete()
event.places = 4
event.waiting_list_places = 2
event.save()
resp = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
assert resp.json['err'] == 0
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
resp = app.post('/api/agenda/%s/fillslot/%s/?count=2' % (agenda.slug, event.id))
assert resp.json['err'] == 0
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
def test_multiple_booking_api_fillslots(app, some_data, user):
agenda = Agenda.objects.filter(label='Foo bar')[0]
# get slots of first 2 events
events = [
x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period()
][:2]
events_slugs = [x.slug for x in events]
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_slugs]
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslots/?count=NaN' % agenda.slug, params={'slots': slots}, status=400)
assert resp.json['err'] == 1
assert resp.json['reason'] == "invalid value for count (NaN)" # legacy
assert resp.json['err_class'] == "invalid value for count (NaN)"
assert resp.json['err_desc'] == "invalid value for count (NaN)"
resp = app.post(
'/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 'NaN'}, status=400
)
assert resp.json['err'] == 1
assert resp.json['reason'] == "invalid payload" # legacy
assert resp.json['err_class'] == "invalid payload"
assert resp.json['err_desc'] == "invalid payload"
assert 'count' in resp.json['errors']
# get 3 places on 2 slots
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '3'})
# one booking with 5 children
booking = Booking.objects.get(id=resp.json['booking_id'])
cancel_url = resp.json['api']['cancel_url']
assert Booking.objects.filter(primary_booking=booking).count() == 5
assert resp.json['datetime'] == localtime(events[0].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
assert 'accept_url' in resp.json['api']
assert 'cancel_url' in resp.json['api']
assert 'ics_url' in resp.json['api']
resp_events = resp.json['events']
assert len(resp_events) == len(events)
for (e, resp_e) in zip(events, resp_events):
assert e.slug == resp_e['slug']
assert e.description == resp_e['description']
assert str(e) == resp_e['text']
assert localtime(e.start_datetime).strftime('%Y-%m-%d %H:%M:%S') == resp_e['datetime']
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 2})
for event in events:
assert Event.objects.get(id=event.id).booked_places == 5
resp = app.post(cancel_url)
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
# check available places overflow
# NB: limit only the first event !
events[0].places = 3
events[0].waiting_list_places = 8
events[0].save()
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
accept_url = resp.json['api']['accept_url']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
# accept the waiting list
resp = app.post(accept_url)
for event in events:
assert Event.objects.get(id=event.id).booked_places == 7
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
# check with a short waiting list
Booking.objects.all().delete()
# NB: limit only the first event !
events[0].places = 4
events[0].waiting_list_places = 2
events[0].save()
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3})
assert resp.json['err'] == 0
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out' # legacy
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '2'})
assert resp.json['err'] == 0
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
def test_multiple_booking_move_booking(app, user):
agenda = Agenda(label='Foo bar')
agenda.save()
first_date = localtime(now()).replace(hour=17, minute=0, second=0, microsecond=0)
first_date += datetime.timedelta(days=1)
events = []
for i in range(10):
event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda)
event.save()
events.append(event)
first_two_events = events[:2]
events_slugs = [x.slug for x in first_two_events]
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_slugs]
app.authorization = ('Basic', ('john.doe', 'password'))
# get 1 place on 2 slots
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
booking = Booking.objects.get(id=resp.json['booking_id'])
assert Booking.objects.filter(primary_booking=booking).count() == 1
for event in first_two_events:
assert Event.objects.get(id=event.id).booked_places == 1
# change, 1 place on 2 other slots
last_two_events = events[-2:]
events_slugs = [x.slug for x in last_two_events]
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_slugs]
resp = app.post(
'/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'cancel_booking_id': booking.pk}
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert Booking.objects.filter(primary_booking=booking).count() == 1
for event in first_two_events:
assert Event.objects.get(id=event.id).booked_places == 0
for event in last_two_events:
assert Event.objects.get(id=event.id).booked_places == 1
def test_agenda_meeting_api_multiple_desk(app, meetings_agenda, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
# add booking of another meeting type
meeting_type2 = MeetingType.objects.create(agenda=meetings_agenda, label='Tux kart', duration=60)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type2.id)
event_id = resp.json['data'][0]['id']
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
cancel_url = resp.json['api']['cancel_url']
# add a second desk
time_period = meetings_agenda.desk_set.first().timeperiod_set.first()
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda)
TimePeriod.objects.create(
start_time=time_period.start_time,
end_time=time_period.end_time,
weekday=time_period.weekday,
desk=desk2,
)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
event_id = resp.json['data'][1]['id']
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert Booking.objects.count() == 2
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
)
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x['disabled']]) + 1
# try booking the same timeslot and fail
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert Booking.objects.count() == 2
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available' # legacy
assert resp.json['err_class'] == 'no more desk available'
assert resp.json['err_desc'] == 'no more desk available'
# cancel first booking and retry
resp = app.post(cancel_url)
# capture number of queries made for datetime endpoint with few bookings
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
queries_count_datetime1 = len(ctx.captured_queries)
assert len(resp2.json['data']) == len([x for x in resp.json['data'] if not x['disabled']])
# capture number of queries made for fillslot endpoint with few bookings
with CaptureQueriesContext(connection) as ctx:
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
queries_count_fillslot1 = len(ctx.captured_queries)
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
)
cancel_url = resp.json['api']['cancel_url']
resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp2.json['data']) == len([x for x in resp3.json['data'] if not x['disabled']]) + 1
# cancel a booking
resp = app.post(cancel_url)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len(resp2.json['data'])
# try booking the same slot to make sure that cancelled booking has freed the slot
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert Booking.objects.count() == 4
assert Booking.objects.exclude(cancellation_datetime__isnull=True).count() == 2
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime(
'%Y-%m-%d %H:%M:%S'
)
# try booking the same timeslot again and fail
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available' # legacy
assert resp.json['err_class'] == 'no more desk available'
assert resp.json['err_desc'] == 'no more desk available'
# fill the agenda and make sure big O is O(1)
for event_data in resp2.json['data'][2:10]:
booking_url = event_data['api']['fillslot_url']
with CaptureQueriesContext(connection) as ctx:
app.post(booking_url)
assert len(ctx.captured_queries) == queries_count_fillslot1
with CaptureQueriesContext(connection) as ctx:
app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert queries_count_datetime1 == len(ctx.captured_queries)
def test_agenda_meeting_api_fillslots_multiple_desks(app, meetings_agenda, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda_id = meetings_agenda.slug
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
# add a second desk, same timeperiods
time_period = meetings_agenda.desk_set.first().timeperiod_set.first()
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda)
TimePeriod.objects.create(
start_time=time_period.start_time,
end_time=time_period.end_time,
weekday=time_period.weekday,
desk=desk2,
)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
slots = [x['id'] for x in resp.json['data'][:3]]
def get_free_places():
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
return len([x for x in resp.json['data'] if not x['disabled']])
start_free_places = get_free_places()
# booking 3 slots on desk 1
fillslots_url = '/api/agenda/%s/fillslots/' % agenda_id
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 0
desk1 = resp.json['desk']['slug']
cancel_url = resp.json['api']['cancel_url']
assert get_free_places() == start_free_places
# booking same slots again, will be on desk 2
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 0
assert resp.json['desk']['slug'] != desk2
# 3 places are disabled in datetimes list
assert get_free_places() == start_free_places - len(slots)
# try booking again: no desk available
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available' # legacy
assert resp.json['err_class'] == 'no more desk available'
assert resp.json['err_desc'] == 'no more desk available'
assert get_free_places() == start_free_places - len(slots)
# cancel desk 1 booking
resp = app.post(cancel_url)
assert resp.json['err'] == 0
# all places are free again
assert get_free_places() == start_free_places
# booking a single slot (must be on desk 1)
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, slots[1]))
assert resp.json['err'] == 0
assert resp.json['desk']['slug'] == desk1
cancel_url = resp.json['api']['cancel_url']
assert get_free_places() == start_free_places - 1
# try booking the 3 slots again: no desk available, one slot is not fully available
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 1
assert resp.json['reason'] == 'no more desk available' # legacy
assert resp.json['err_class'] == 'no more desk available'
assert resp.json['err_desc'] == 'no more desk available'
# cancel last signel slot booking, desk1 will be free
resp = app.post(cancel_url)
assert resp.json['err'] == 0
assert get_free_places() == start_free_places
# booking again is ok, on desk 1
resp = app.post(fillslots_url, params={'slots': slots})
assert resp.json['err'] == 0
assert resp.json['desk']['slug'] == desk1
assert get_free_places() == start_free_places - len(slots)
def test_agenda_meeting_same_day(app, meetings_agenda, mock_now, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda(label='Foo', kind='meetings')
agenda.minimal_booking_delay = 0
agenda.maximal_booking_delay = 15
agenda.save()
meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
datetime_url = '/api/agenda/meetings/%s/datetimes/' % meeting_type.id
desk1 = Desk.objects.create(label='foo', agenda=agenda)
desk2 = Desk.objects.create(label='bar', agenda=agenda)
for weekday in range(7):
TimePeriod.objects.create(
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk1
)
TimePeriod.objects.create(
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk2
)
resp = app.get(datetime_url)
event_data = resp.json['data'][0]
# check first proposed date is on the same day unless we're past the last
# open timeperiod.
event_datetime = datetime.datetime.strptime(event_data['datetime'], '%Y-%m-%d %H:%M:%S').timetuple()
assert (
event_datetime[:3] == mock_now.timetuple()[:3] and event_datetime[3:5] >= mock_now.timetuple()[3:5]
) or (event_datetime[:3] > mock_now.timetuple()[:3] and event_datetime[3:5] < mock_now.timetuple()[3:5])
# check booking works
first_booking_url = resp.json['data'][0]['api']['fillslot_url']
assert app.post(first_booking_url).json['err'] == 0
assert app.post(first_booking_url).json['err'] == 0
assert app.post(first_booking_url).json['err'] == 1
last_booking_url = resp.json['data'][-1]['api']['fillslot_url']
assert app.post(last_booking_url).json['err'] == 0
assert app.post(last_booking_url).json['err'] == 0
assert app.post(last_booking_url).json['err'] == 1
# check full datetimes are marked as disabled
resp = app.get(datetime_url)
assert resp.json['data'][0]['disabled']
assert not resp.json['data'][1]['disabled']
assert resp.json['data'][-1]['disabled']
assert not resp.json['data'][-2]['disabled']
def test_virtual_agendas_meetings_booking(app, mock_now, user):
foo_agenda = Agenda.objects.create(
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=foo_desk_1,
)
TimePeriod.objects.create(
weekday=1,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=foo_desk_1,
)
bar_agenda = Agenda.objects.create(
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1')
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=bar_desk_1,
)
TimePeriod.objects.create(
weekday=1,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=bar_desk_1,
)
virt_agenda = Agenda.objects.create(
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
# We are saturday and we can book for next monday and tuesday, 4 slots available each day
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
resp = app.get(api_url)
assert len(resp.json['data']) == 8
# make a booking
fillslot_url = resp.json['data'][0]['api']['fillslot_url']
app.authorization = ('Basic', ('john.doe', 'password'))
resp_booking = app.post(fillslot_url)
assert Booking.objects.count() == 1
booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
assert (
resp_booking.json['datetime']
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
== resp.json['data'][0]['datetime']
)
first_booking_agenda = resp_booking.json['agenda']['slug']
assert resp_booking.json['end_datetime'] == localtime(
Booking.objects.all()[0].event.end_datetime
).strftime('%Y-%m-%d %H:%M:%S')
assert resp_booking.json['duration'] == 30
# second booking on the same slot (available on the second real agenda)
resp_booking = app.post(fillslot_url)
assert Booking.objects.count() == 2
booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
assert (
resp_booking.json['datetime']
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
== resp.json['data'][0]['datetime']
)
second_booking_agenda = resp_booking.json['agenda']['slug']
assert {first_booking_agenda, second_booking_agenda} == {'foo-meeting', 'bar-meeting'}
# try booking the same timeslot a third time: full
resp_booking = app.post(fillslot_url)
assert resp_booking.json['err'] == 1
assert resp_booking.json['err_class'] == 'no more desk available'
assert resp_booking.json['err_desc'] == 'no more desk available'
@pytest.mark.freeze_time('2021-02-25')
def test_virtual_agendas_meetings_booking_exclude_slots(app, user):
tomorrow = now() + datetime.timedelta(days=1)
agenda = Agenda.objects.create(
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10
)
desk = Desk.objects.create(agenda=agenda, slug='desk')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
TimePeriod.objects.create(
weekday=tomorrow.date().weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
agenda2 = agenda.duplicate()
agenda3 = agenda.duplicate()
virt_agenda = Agenda.objects.create(
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=10
)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda3)
event = Event.objects.create(
agenda=agenda,
meeting_type=meeting_type,
places=1,
start_datetime=localtime(tomorrow).replace(hour=9, minute=0),
desk=desk,
)
Booking.objects.create(event=event, user_external_id='42')
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post(
'/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
params={'user_external_id': '42'},
)
assert resp.json['err'] == 0
resp = app.post(
'/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
params={'user_external_id': '42', 'exclude_user': True},
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'no more desk available'
resp = app.post(
'/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
params={'exclude_user': True},
)
assert resp.json['err'] == 0
virt_agenda.minimal_booking_delay = None
virt_agenda.maximal_booking_delay = None
virt_agenda.save()
resp = app.post(
'/api/agenda/%s/fillslot/%s:2021-02-26-0900/' % (virt_agenda.slug, meeting_type.slug),
params={'user_external_id': '42', 'exclude_user': True},
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'no more desk available'
def test_virtual_agendas_meetings_booking_default_policy(app, mock_now, user):
foo_agenda = Agenda.objects.create(
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2')
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=foo_desk_1,
)
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=foo_desk_2,
)
bar_agenda = Agenda.objects.create(
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1')
bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2')
bar_desk_3 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 3')
bar_desk_4 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 3')
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=bar_desk_1,
)
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=bar_desk_2,
)
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=bar_desk_3,
)
TimePeriod.objects.create(
weekday=0,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=bar_desk_4,
)
virt_agenda = Agenda.objects.create(
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
# We are saturday and we can book for next monday and tuesday, 4 slots available each day
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
resp = app.get(api_url)
# We are saturday and we can book for next monday, 4 slots available each day
assert len(resp.json['data']) == 4
# there are 6 desks so we can make 6 bookings on the same slot
fillslot_url = resp.json['data'][0]['api']['fillslot_url']
app.authorization = ('Basic', ('john.doe', 'password'))
for i in range(1, 7):
foo_num_bookings = Booking.objects.filter(event__desk__agenda=foo_agenda).count()
bar_num_bookings = Booking.objects.filter(event__desk__agenda=bar_agenda).count()
foo_fill_rate = foo_num_bookings / 2
bar_fill_rate = bar_num_bookings / 4
next_agenda = None
if i != 1:
if foo_fill_rate < bar_fill_rate:
next_agenda = foo_agenda
elif foo_fill_rate > bar_fill_rate:
next_agenda = bar_agenda
elif foo_fill_rate == bar_fill_rate:
next_agenda = None
resp_booking = app.post(fillslot_url)
assert Booking.objects.count() == i
booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
assert (
resp_booking.json['datetime']
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
== resp.json['data'][0]['datetime']
)
if next_agenda:
assert booking.event.agenda == next_agenda
foo_num_bookings = Booking.objects.filter(event__desk__agenda=foo_agenda).count()
bar_num_bookings = Booking.objects.filter(event__desk__agenda=bar_agenda).count()
assert foo_num_bookings == 2
assert bar_num_bookings == 4
@pytest.mark.freeze_time('2017-04-01')
def test_duration_on_booking_api_fillslot_response(app, user):
agenda = Agenda(label='Foo bar')
agenda.save()
first_date = datetime.datetime(2017, 5, 20, 1, 12)
durations = [None, 0, 45]
evt = []
for i in range(3):
evt.append(
Event(
start_datetime=first_date + datetime.timedelta(days=i),
duration=durations[i],
places=20,
agenda=agenda,
)
)
evt[i].save()
assert evt[0].end_datetime is None
assert evt[1].end_datetime == evt[1].start_datetime
assert evt[2].end_datetime == evt[2].start_datetime + datetime.timedelta(minutes=45)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evt[0].id))
assert resp.json['datetime'] == '2017-05-20 01:12:00'
assert resp.json['end_datetime'] is None
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART:20170519T231200Z' in ics
assert 'DTEND:' not in ics
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evt[1].id))
assert resp.json['datetime'] == '2017-05-21 01:12:00'
assert resp.json['end_datetime'] == resp.json['datetime']
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART:20170520T231200Z' in ics
assert 'DTEND:20170520T231200Z' in ics
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, evt[2].id))
assert resp.json['datetime'] == '2017-05-22 01:12:00'
assert resp.json['end_datetime'] == '2017-05-22 01:57:00'
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART:20170521T231200Z' in ics
assert 'DTEND:20170521T235700Z' in ics
@pytest.mark.freeze_time('2017-04-01')
def test_duration_on_booking_api_fillslots_response(app, user):
agenda = Agenda(label='Foo bar')
agenda.save()
first_date = datetime.datetime(2017, 5, 20, 1, 12)
durations = [None, 0, 45]
evt = []
for i in range(3):
evt.append(
Event(
start_datetime=first_date + datetime.timedelta(days=i),
duration=durations[i],
places=20,
agenda=agenda,
)
)
evt[i].save()
assert evt[0].end_datetime is None
assert evt[1].end_datetime == evt[1].start_datetime
assert evt[2].end_datetime == evt[2].start_datetime + datetime.timedelta(minutes=45)
app.authorization = ('Basic', ('john.doe', 'password'))
# first event having null duration
string_param = ','.join([str(e.id) for e in evt[::-1]]) # unordered parameters
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': string_param})
r_evt = resp.json['events']
assert r_evt[0]['datetime'] == '2017-05-20 01:12:00'
assert r_evt[0]['end_datetime'] is None
assert r_evt[1]['datetime'] == '2017-05-21 01:12:00'
assert r_evt[1]['end_datetime'] == r_evt[1]['datetime']
assert r_evt[2]['datetime'] == '2017-05-22 01:12:00'
assert r_evt[2]['end_datetime'] == '2017-05-22 01:57:00'
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART:20170519T231200Z' in ics
assert 'DTEND:' not in ics
# first event having duration
evt[0].duration = 90
evt[0].save()
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': string_param})
r_evt = resp.json['events']
assert r_evt[0]['datetime'] == '2017-05-20 01:12:00'
assert r_evt[0]['end_datetime'] == '2017-05-20 02:42:00'
assert 'ics_url' in resp.json['api']
ics = app.get(resp.json['api']['ics_url']).text
assert 'DTSTART:20170519T231200Z' in ics
assert 'DTEND:20170520T004200Z' in ics
def test_fillslot_past_event(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
event1 = Event.objects.create(
label='Today before now',
start_datetime=localtime(now() - datetime.timedelta(hours=1)),
places=5,
agenda=agenda,
)
event2 = Event.objects.create(
label='Today after now',
start_datetime=localtime(now() + datetime.timedelta(hours=1)),
places=5,
agenda=agenda,
)
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug))
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'future'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'past'})
assert resp.json['err'] == 0
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'all'})
assert resp.json['err'] == 0
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug))
assert resp.json['err'] == 0
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'future'})
assert resp.json['err'] == 0
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'past'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event2.slug
resp = app.post('/api/agenda/%s/fillslot/%s/?events=past' % (agenda.slug, event2.slug))
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event2.slug
resp = app.post(
'/api/agenda/%s/fillslot/%s/?events=all' % (agenda.slug, event2.slug), params={'events': 'past'}
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event2.slug
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'all'})
assert resp.json['err'] == 0
# check canceled
event1.cancel()
event2.cancel()
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event1.slug), params={'events': 'all'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is cancelled' % event1.slug
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event2.slug), params={'events': 'all'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is cancelled' % event2.slug
def test_fillslot_past_event_places(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
event = Event.objects.create(
label='Today before now',
start_datetime=localtime(now() - datetime.timedelta(hours=1)),
places=1,
agenda=agenda,
)
Booking.objects.create(event=event)
# always bookable if past
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'events': 'past'})
assert resp.json['err'] == 0
event.waiting_list_places = 1
event.save()
Booking.objects.create(event=event, in_waiting_list=True)
# always bookable if past
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'events': 'past'})
assert resp.json['err'] == 0
def test_fillslot_past_events_min_places(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
event = Event.objects.create(
label='Today before now',
start_datetime=localtime(now() - datetime.timedelta(hours=1)),
places=1,
agenda=agenda,
)
# always bookable if past
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug), params={'events': 'past', 'count': 2}
)
assert resp.json['err'] == 0
def test_fillslot_past_events_exclude_slots(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
event = Event.objects.create(
label='Today before now',
start_datetime=localtime(now() - datetime.timedelta(hours=1)),
places=5,
agenda=agenda,
)
Booking.objects.create(event=event, user_external_id='42')
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
params={'events': 'past', 'user_external_id': '35', 'exclude_user': True},
)
assert resp.json['err'] == 0
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
params={'events': 'past', 'user_external_id': '35', 'exclude_user': True},
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is already booked by user' % event.slug
def test_fillslot_past_events_recurring_event(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
start_datetime = localtime(now() - datetime.timedelta(days=3 * 7))
event = Event.objects.create(
label='Recurring',
start_datetime=start_datetime,
recurrence_days=[start_datetime.weekday()],
places=5,
agenda=agenda,
)
event_slug = '%s:%s' % (
event.slug,
(event.start_datetime - datetime.timedelta(days=7)).strftime('%Y-%m-%d-%H%M'),
) # too soon
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'}, status=400
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'invalid datetime for event %s' % event_slug
event_slug = '%s:%s' % (
event.slug,
(event.start_datetime + datetime.timedelta(days=7)).strftime('%Y-%m-%d-%H%M'),
)
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'past'})
assert resp.json['err'] == 0
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'})
assert resp.json['err'] == 0
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'future'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event_slug.replace(':', '--')
event_slug = '%s:%s' % (
event.slug,
(event.start_datetime + datetime.timedelta(days=50 * 7)).strftime('%Y-%m-%d-%H%M'),
) # too late
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event_slug.replace(':', '--')
event_slug = '%s:%s' % (
event.slug,
(event.start_datetime + datetime.timedelta(days=4 * 7)).strftime('%Y-%m-%d-%H%M'),
)
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'past'})
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event_slug.replace(':', '--')
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'all'})
assert resp.json['err'] == 0
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_slug), params={'events': 'future'})
assert resp.json['err'] == 0
# check exclude_user_external_id
first_recurrence = event.get_or_create_event_recurrence(event.start_datetime)
Booking.objects.create(event=first_recurrence, user_external_id='42')
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, first_recurrence.slug),
params={'events': 'past', 'user_external_id': '42', 'exclude_user': True},
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is already booked by user' % first_recurrence.slug
# check canceled
first_recurrence.cancel()
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, first_recurrence.slug), params={'events': 'past'}
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is cancelled' % first_recurrence.slug
def test_recurring_events_api_fillslots(app, user, freezer):
freezer.move_to('2021-09-06 12:00')
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
label='Event',
start_datetime=now(),
recurrence_days=[0, 1, 3, 4], # Monday, Tuesday, Thursday, Friday
places=2,
waiting_list_places=1,
agenda=agenda,
recurrence_end_date=now() + datetime.timedelta(days=364),
)
event.create_all_recurrences()
sunday_event = Event.objects.create(
label='Sunday Event',
start_datetime=now(),
recurrence_days=[6],
places=2,
waiting_list_places=1,
agenda=agenda,
recurrence_end_date=now() + datetime.timedelta(days=364),
)
sunday_event.create_all_recurrences()
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
assert len(resp.json['data']) == 5
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug
params = {'user_external_id': 'user_id'}
# Book Monday and Thursday of first event and Sunday of second event
params['slots'] = 'foo-bar@event:0,foo-bar@event:3,foo-bar@sunday-event:6'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 156
assert Booking.objects.count() == 156
assert Booking.objects.filter(event__primary_event=event).count() == 104
assert Booking.objects.filter(event__primary_event=sunday_event).count() == 52
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=1).count() == 156
# one recurrence is booked separately
event = Event.objects.filter(primary_event__isnull=False).first()
Booking.objects.create(event=event)
params['user_external_id'] = 'user_id_2'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 156
assert not resp.json['full_events']
assert Booking.objects.count() == 313
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=2).count() == 156
# one booking has been put in waiting list
assert events.filter(booked_waiting_list_places=1).count() == 1
params['user_external_id'] = 'user_id_3'
resp = app.post_json(fillslots_url, params=params)
# everything goes in waiting list
assert events.filter(booked_waiting_list_places=1).count() == 156
# but an event was full
assert resp.json['booking_count'] == 155
assert len(resp.json['full_events']) == 1
assert resp.json['full_events'][0]['slug'] == event.slug
resp = app.post_json(fillslots_url, params=params)
# events are full
params['user_external_id'] = 'user_id_4'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
# no event in range
resp = app.post_json(fillslots_url + '&date_start=2020-10-06&date_end=2020-11-06', params=params)
assert resp.json['booking_count'] == 0
params['slots'] = 'foo-bar@event:1'
resp = app.post_json(fillslots_url + '&date_start=2021-10-06&date_end=2021-11-06', params=params)
assert resp.json['booking_count'] == 4
assert Booking.objects.filter(user_external_id='user_id_4').count() == 4
resp = app.post_json(fillslots_url, params={'slots': 'foo-bar@event:0'}, status=400)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['user_external_id'] == ['This field is required.']
resp = app.post_json(fillslots_url, params={'user_external_id': 'a'}, status=400)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'invalid payload'
assert resp.json['errors']['slots'] == ['This field is required.']
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:a'}, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['slots'] == ['invalid slot: foo-bar@a:a']
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:1'}, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['slots'] == ['event a of agenda foo-bar is not bookable']
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar'}, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['slots'] == ['Invalid format for slot foo-bar']
def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
freezer.move_to('2021-09-06 12:00')
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
label='Event',
start_datetime=now(),
recurrence_days=[0],
places=2,
waiting_list_places=2,
agenda=agenda,
recurrence_end_date=now() + datetime.timedelta(days=30),
)
event.create_all_recurrences()
app.authorization = ('Basic', ('john.doe', 'password'))
# create bookings in waiting list
for recurrence in event.recurrences.all():
Booking.objects.create(event=recurrence, in_waiting_list=True)
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_waiting_list_places=1).count() == 5
# check that new bookings are put in waiting list despite free slots on main list
params = {'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'}
resp = app.post_json('/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug, params=params)
assert resp.json['booking_count'] == 5
assert events.filter(booked_waiting_list_places=2).count() == 5
def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
freezer.move_to('2021-09-06 12:00')
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
label='Event',
start_datetime=now(),
recurrence_days=[0, 1, 3, 4], # Monday, Tuesday, Thursday, Friday
places=1,
waiting_list_places=1,
agenda=agenda,
recurrence_end_date=now() + datetime.timedelta(days=364),
)
event.create_all_recurrences()
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug
params = {'user_external_id': 'user_id'}
# Book Monday and Thursday
params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 104
assert resp.json['cancelled_booking_count'] == 0
assert Booking.objects.count() == 104
assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 52
assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52
# Change booking to Monday and Tuesday
params['slots'] = 'foo-bar@event:0,foo-bar@event:1'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 52
assert resp.json['cancelled_booking_count'] == 52
assert Booking.objects.count() == 104
assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 52
assert Booking.objects.filter(event__start_datetime__week_day=3).count() == 52
# Booking again does nothing
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 0
assert Booking.objects.count() == 104
params = {'user_external_id': 'user_id_2'}
params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 104
assert resp.json['cancelled_booking_count'] == 0
assert Booking.objects.count() == 208
assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 104
assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=1).count() == 156
assert events.filter(booked_waiting_list_places=1).count() == 52
params['slots'] = 'foo-bar@event:1,foo-bar@event:4'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 104
assert resp.json['cancelled_booking_count'] == 104
assert Booking.objects.count() == 208
assert Booking.objects.filter(event__start_datetime__week_day=3).count() == 104
assert Booking.objects.filter(event__start_datetime__week_day=6).count() == 52
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=1).count() == 156
assert events.filter(booked_waiting_list_places=1).count() == 52
# passing empty slots cancels all bookings
params['slots'] = ''
resp = app.post_json(fillslots_url, params=params)
assert resp.json['cancelled_booking_count'] == 104
assert Booking.objects.filter(user_external_id='user_id_2').count() == 0
# only recurring events are impacted
normal_event = Event.objects.create(
start_datetime=now() + datetime.timedelta(days=1), places=2, agenda=agenda
)
Booking.objects.create(event=normal_event, user_external_id='user_id')
resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'})
assert resp.json['cancelled_booking_count'] == 52
assert Booking.objects.filter(user_external_id='user_id', event=normal_event).count() == 1
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_fillslots_multiple_agendas(app, user):
agenda = Agenda.objects.create(label='First Agenda', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
event_a = Event.objects.create(
label='A',
start_datetime=start,
places=2,
recurrence_end_date=end,
recurrence_days=[0, 2, 5],
agenda=agenda,
)
event_a.create_all_recurrences()
event_b = Event.objects.create(
label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1], agenda=agenda
)
event_b.create_all_recurrences()
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
Desk.objects.create(agenda=agenda2, slug='_exceptions_holder')
event_c = Event.objects.create(
label='C',
start_datetime=start,
places=2,
recurrence_end_date=end,
recurrence_days=[2, 3],
agenda=agenda2,
)
event_c.create_all_recurrences()
resp = app.get('/api/agendas/recurring-events/?agendas=first-agenda,second-agenda')
assert len(resp.json['data']) == 6
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s'
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:0,first-agenda@a:5,second-agenda@c:3'}
resp = app.post_json(fillslots_url % 'first-agenda,second-agenda', params=params)
assert resp.json['booking_count'] == 13
assert Booking.objects.count() == 13
assert Booking.objects.filter(event__primary_event=event_a).count() == 9
assert Booking.objects.filter(event__primary_event=event_b).count() == 0
assert Booking.objects.filter(event__primary_event=event_c).count() == 4
# update bookings
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@b:1'}
resp = app.post_json(fillslots_url % 'first-agenda,second-agenda', params=params)
assert resp.json['booking_count'] == 5
assert resp.json['cancelled_booking_count'] == 13
assert Booking.objects.filter(event__primary_event=event_a).count() == 0
assert Booking.objects.filter(event__primary_event=event_b).count() == 5
assert Booking.objects.filter(event__primary_event=event_c).count() == 0
# error if slot's agenda is not in querystring
resp = app.post_json(fillslots_url % 'second-agenda', params=params, status=400)
assert resp.json['err'] == 1
assert resp.json['errors']['slots'] == [
'Some events belong to agendas that are not present in querystring: first-agenda'
]
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
for i in range(20):
agenda = Agenda.objects.create(slug=f'{i}', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
start, end = now(), now() + datetime.timedelta(days=30)
event = Event.objects.create(
start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1, 2], agenda=agenda
)
event.create_all_recurrences()
agenda_slugs = ','.join(str(i) for i in range(20))
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda_slugs)
events_to_book = [x['id'] for x in resp.json['data']]
app.authorization = ('Basic', ('john.doe', 'password'))
with CaptureQueriesContext(connection) as ctx:
resp = app.post_json(
'/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda_slugs,
params={'slots': events_to_book, 'user_external_id': 'user'},
)
assert resp.json['booking_count'] == 180
assert len(ctx.captured_queries) == 16
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=1),
places=2,
waiting_list_places=1,
agenda=agenda,
)
second_event = Event.objects.create(
label='Event 2', start_datetime=now() + datetime.timedelta(days=2), places=2, agenda=agenda
)
third_event = Event.objects.create(
label='Event 3', start_datetime=now() + datetime.timedelta(days=3), places=2, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
params = {'user_external_id': 'user_id', 'slots': 'event,event-2'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 2
assert len(resp.json['waiting_list_events']) == 0
events = Event.objects.all()
assert events.filter(booked_places=1).count() == 2
params['user_external_id'] = 'user_id_2'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 2
assert len(resp.json['waiting_list_events']) == 0
params['user_external_id'] = 'user_id_3'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'some events are full: Event 2'
params['slots'] = 'event'
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 1
assert resp.json['waiting_list_events'][0]['slug'] == event.slug
assert Booking.objects.filter(in_waiting_list=True, event=event).count() == 1
# change bookings
params = {'user_external_id': 'user_id', 'slots': 'event-2,event-3'}
resp = app.post_json(fillslots_url, params=params)
assert resp.json['booking_count'] == 1
assert resp.json['cancelled_booking_count'] == 1
user_bookings = Booking.objects.filter(user_external_id='user_id')
assert {b.event.slug for b in user_bookings} == {'event-2', 'event-3'}
assert event.booking_set.count() == 2
assert second_event.booking_set.count() == 2
assert third_event.booking_set.count() == 1
# increase waiting_list_places to make "Event" bookable again
event.waiting_list_places = 2
event.save()
# specify time range so that "Event 3" is not cancelled
params['slots'] = 'event,event-2'
resp = app.post_json(fillslots_url + '?date_start=2021-09-06&date_end=2021-09-09', params=params)
assert resp.json['booking_count'] == 1
assert resp.json['cancelled_booking_count'] == 0
user_bookings = Booking.objects.filter(user_external_id='user_id')
assert {b.event.slug for b in user_bookings} == {'event', 'event-2', 'event-3'}
assert event.booking_set.count() == 3
assert second_event.booking_set.count() == 2
assert third_event.booking_set.count() == 1
# new event booking went in waiting list despite free slots on main list
assert Booking.objects.filter(in_waiting_list=True, event=event).count() == 2
# passing empty slots cancels all bookings
params['slots'] = ''
resp = app.post_json(fillslots_url, params=params)
assert resp.json['cancelled_booking_count'] == 3
assert Booking.objects.filter(user_external_id='user_id').count() == 0
resp = app.post('/api/agenda/foobar/events/fillslots/', status=404)
resp = app.post('/api/agenda/0/events/fillslots/', status=404)
def test_api_events_fillslots_check_delays(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
)
Event.objects.create(
slug='event-slug',
start_datetime=localtime() + datetime.timedelta(days=5),
places=5,
agenda=agenda,
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslots_url = '/api/agenda/%s/events/fillslots/' % agenda.slug
resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
assert resp.json['err'] == 0
# test minimal_booking_delay
agenda.minimal_booking_delay = 6
agenda.save()
resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post(
fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug', 'bypass_delays': True}
)
assert resp.json['err'] == 0
# test maximal_booking_delay
agenda.minimal_booking_delay = 0
agenda.maximal_booking_delay = 3
agenda.save()
resp = app.post(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug'})
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post(
fillslots_url, params={'user_external_id': 'user_id', 'slots': 'event-slug', 'bypass_delays': True}
)
assert resp.json['err'] == 0
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_past_event(app, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
event1 = Event.objects.create(
label='Today before now',
start_datetime=localtime(now() - datetime.timedelta(hours=1)),
places=5,
agenda=agenda,
)
event2 = Event.objects.create(
label='Today after now',
start_datetime=localtime(now() + datetime.timedelta(hours=1)),
places=5,
agenda=agenda,
)
params = {'user_external_id': 'user_id', 'slots': ','.join((event1.slug, event2.slug))}
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
params['events'] = 'future'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
params['events'] = 'past'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event2.slug
params['events'] = 'all'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 0
@pytest.mark.freeze_time('2021-09-06 12:00')
def test_api_events_fillslots_multiple_agendas(app, user):
first_agenda = Agenda.objects.create(label='First agenda', kind='events')
Desk.objects.create(agenda=first_agenda, slug='_exceptions_holder')
first_event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=5),
places=2,
agenda=first_agenda,
)
second_agenda = Agenda.objects.create(label='Second agenda', kind='events')
Desk.objects.create(agenda=second_agenda, slug='_exceptions_holder')
second_event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=6),
places=2,
agenda=second_agenda,
)
agenda_slugs = '%s,%s' % (first_agenda.slug, second_agenda.slug)
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda_slugs})
event_slugs = ','.join((resp.json['data'][0]['id'], resp.json['data'][1]['id']))
assert event_slugs == 'first-agenda@event,second-agenda@event'
app.authorization = ('Basic', ('john.doe', 'password'))
params = {'user_external_id': 'user_id', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['booking_count'] == 2
assert first_event.booking_set.count() == 1
assert second_event.booking_set.count() == 1
# booking modification
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@event'}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['booking_count'] == 0
assert resp.json['cancelled_booking_count'] == 1
assert first_event.booking_set.count() == 1
assert second_event.booking_set.count() == 0
params = {'user_external_id': 'user_id_2', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['booking_count'] == 2
assert first_event.booking_set.count() == 2
assert second_event.booking_set.count() == 1
params = {'user_external_id': 'user_id_3', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'some events are full: Event'
# invalid agenda slugs in querystring
resp = app.post_json(
'/api/agendas/events/fillslots/?agendas=first-agenda,xxx,yyy', params=params, status=400
)
assert resp.json['errors']['agendas'][0] == 'invalid slugs: xxx, yyy'
# invalid agenda slugs in payload
params = {'user_external_id': 'user_id_3', 'slots': 'first-agenda@event,xxx@event,yyy@event'}
resp = app.post_json(
'/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params, status=400
)
assert resp.json['errors']['slots'] == [
'Some events belong to agendas that are not present in querystring: xxx, yyy'
]
# missing agendas parameter
resp = app.post_json('/api/agendas/events/fillslots/', params=params, status=400)
assert resp.json['errors']['agendas'] == ['This field is required.']
# valid agendas parameter and event slugs, but mismatch between the two
params = {'user_external_id': 'user_id_3', 'slots': event_slugs}
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == [
'Some events belong to agendas that are not present in querystring: second-agenda'
]
# missing @ in slot
params['slots'] = 'first-agenda'
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == ['Invalid format for slot first-agenda']
# empty event slug
params['slots'] = 'first-agenda@'
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == ['Missing event slug in slot first-agenda@']
# empty agenda slug
params['slots'] = '@event'
resp = app.post_json('/api/agendas/events/fillslots/?agendas=first-agenda', params=params, status=400)
assert resp.json['errors']['slots'] == ['Missing agenda slug in slot @event']
def test_api_events_fillslots_multiple_agendas_check_delays(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
)
Event.objects.create(
slug='event-slug',
start_datetime=localtime() + datetime.timedelta(days=5),
places=5,
agenda=agenda,
)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post(
'/api/agendas/events/fillslots/?agendas=foo-bar',
params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug'},
)
assert resp.json['err'] == 0
# test minimal_booking_delay
agenda.minimal_booking_delay = 6
agenda.save()
resp = app.post(
'/api/agendas/events/fillslots/?agendas=foo-bar',
params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug'},
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post(
'/api/agendas/events/fillslots/?agendas=foo-bar',
params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug', 'bypass_delays': True},
)
assert resp.json['err'] == 0
# test maximal_booking_delay
agenda.minimal_booking_delay = 0
agenda.maximal_booking_delay = 3
agenda.save()
resp = app.post(
'/api/agendas/events/fillslots/?agendas=foo-bar',
params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug'},
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event not bookable'
agenda.save()
resp = app.post(
'/api/agendas/events/fillslots/?agendas=foo-bar',
params={'user_external_id': 'user_id', 'slots': 'foo-bar@event-slug', 'bypass_delays': True},
)
assert resp.json['err'] == 0
def test_url_translation(app, some_data, user):
app.authorization = ('Basic', ('john.doe', 'password'))
agenda_id = Agenda.objects.filter(label='Foo bar')[0].id
assert Booking.objects.count() == 0
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id)
fillslot_url = resp.json['data'][0]['api']['fillslot_url']
params = {
'backoffice_url': 'https://demarches.example.com/backoffice/foo/bar',
'cancel_callback_url': 'https://demarches.example.com/foo/bar/jump',
'form_url': 'https://demarches.example.com/foo/bar',
}
# https://demarches.example.com not in KNOWN_SERVICES, no URL translation
resp = app.post_json(fillslot_url, params=params)
booking = Booking.objects.get(pk=resp.json['booking_id'])
assert booking.backoffice_url == 'https://demarches.example.com/backoffice/foo/bar'
assert booking.cancel_callback_url == 'https://demarches.example.com/foo/bar/jump'
assert booking.form_url == 'https://demarches.example.com/foo/bar'
# http://example.org/ is in KNOWN_SERVICES, translation happens
params = {
'backoffice_url': 'http://example.org/backoffice/foo/bar',
'cancel_callback_url': 'http://example.org/foo/bar/jump',
'form_url': 'http://example.org/foo/bar',
}
resp = app.post_json(fillslot_url, params=params)
booking = Booking.objects.get(pk=resp.json['booking_id'])
assert booking.backoffice_url == 'publik://default/backoffice/foo/bar'
assert booking.cancel_callback_url == 'publik://default/foo/bar/jump'
assert booking.form_url == 'publik://default/foo/bar'
def test_fillslot_recurring_event_booking_forbidden(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=7),
recurrence_days=[now().weekday()],
places=2,
agenda=agenda,
)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event is recurrent'