chrono/tests/api/fillslot/test_all.py

2624 lines
110 KiB
Python

import datetime
import random
import urllib.parse as urlparse
from unittest import mock
import pytest
from django.db import IntegrityError, connection
from django.test.utils import CaptureQueriesContext
from chrono.agendas.models import (
Agenda,
Booking,
BookingColor,
Desk,
Event,
MeetingType,
Resource,
TimePeriod,
VirtualMember,
)
from chrono.utils.timezone import localtime, now
pytestmark = pytest.mark.django_db
def test_booking_api(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
)
# 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': '+33 (0) 6 12 34 56 78', # long phone number
'form_url': 'http://example.net/',
'extra_emails': ['baz@baz.com', 'hop@hop.com'],
'extra_phone_numbers': ['+33123456789', '+33123456789'],
'presence_callback_url': 'http://example.net/jump/trigger2/',
'absence_callback_url': 'http://example.net/jump/trigger3/',
},
)
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.presence_callback_url == 'http://example.net/jump/trigger2/'
assert booking.absence_callback_url == 'http://example.net/jump/trigger3/'
assert booking.user_email == 'bar@bar.com'
assert booking.user_phone_number == '+33 (0) 6 12 34 56 78'
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']
# extra_data list/dict values are refused
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
params={'foo': ['bar', 'baz']},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id),
params={'foo': {'bar': 'baz'}},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
# 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_event_exclusion_constraint(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar')
desk = Desk.objects.create(agenda=agenda, slug='desk')
tomorrow = now() + datetime.timedelta(days=1)
TimePeriod.objects.create(
weekday=tomorrow.date().weekday(),
start_time=datetime.time(9, 0),
end_time=datetime.time(17, 00),
desk=desk,
)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
event_id = resp.json['data'][2]['id']
app.authorization = ('Basic', ('john.doe', 'password'))
original_save = Event.save
global first_call # pylint: disable=global-variable-undefined
first_call = True
def wrapped(*args, **kwargs):
global first_call # pylint: disable=global-variable-undefined
if first_call is True:
first_call = False
raise IntegrityError('tstzrange_constraint')
return original_save(*args, **kwargs)
with mock.patch('chrono.agendas.models.Event.save', autospec=True) as mock_create:
mock_create.side_effect = wrapped
resp = app.post_json(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event_id),
)
assert len(mock_create.call_args_list) == 2
assert resp.json['err'] == 0
assert Event.objects.count() == 1
assert Booking.objects.count() == 1
def test_booking_api_extra_emails(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslot_url = '/api/agenda/%s/fillslot/%s/' % (agenda.id, event.slug)
resp = app.post_json(
fillslot_url,
params={
'extra_emails': ['foo@foo.com', 'bar@bar.com'],
'extra_phone_numbers': ['+33123456789', '+33123456790'],
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['foo@foo.com', 'bar@bar.com']
assert booking.extra_phone_numbers == ['+33123456789', '+33123456790']
resp = app.post_json(
fillslot_url,
params={
'extra_emails': 'foo@foo.com, bar@bar.com',
'extra_phone_numbers': '+33123456789,+33123456790',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['foo@foo.com', 'bar@bar.com']
assert booking.extra_phone_numbers == ['+33123456789', '+33123456790']
resp = app.post_json(
fillslot_url,
params={
'extra_emails': 'foo@foo.com',
'extra_phone_numbers': '+33123456789',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['foo@foo.com']
assert booking.extra_phone_numbers == ['+33123456789']
resp = app.post_json(
fillslot_url,
params={
'extra_emails': ', bar@bar.com',
'extra_phone_numbers': '',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['bar@bar.com']
assert booking.extra_phone_numbers == []
resp = app.post(fillslot_url)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == []
assert booking.extra_phone_numbers == []
resp = app.post_json(
fillslot_url,
params={
'extra_emails': 'bar.com',
'extra_phone_numbers': 'too loooooooooong',
},
status=400,
)
assert resp.json['errors']['extra_emails']['0'] == ['Enter a valid email address.']
assert resp.json['errors']['extra_phone_numbers']['0'] == [
'Ensure this field has no more than 16 characters.'
]
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
booking = Booking.objects.latest('pk')
assert booking.out_of_min_delay is False
# 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
booking = Booking.objects.latest('pk')
assert booking.out_of_min_delay is True
# 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
booking = Booking.objects.latest('pk')
assert booking.out_of_min_delay is False
@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 # ok, no user_external_id
Booking.objects.update(cancellation_datetime=now())
resp = app.post(
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug),
params={'user_external_id': '42', 'exclude_user': True},
)
assert resp.json['err'] == 0 # ok, existing bookings are cancelled
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()],
recurrence_end_date=start_datetime + datetime.timedelta(days=15),
places=3,
agenda=agenda,
)
event.create_all_recurrences()
first_recurrence = Event.objects.get(primary_event=event, start_datetime=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, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
events = []
for i in range(3):
events.append(
Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5 + i), places=20, agenda=agenda
)
)
events_ids = [x.id for x in events]
events_slugs = [x.slug for x in events]
event = events[0]
# 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']
# extra_data list/dict values are refused
resp = app.post_json(
'/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids, 'foo': ['bar', 'baz']},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
resp = app.post_json(
'/api/agenda/%s/fillslots/' % agenda.id,
params={'slots': events_ids, 'foo': {'bar': 'baz'}},
status=400,
)
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
# 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 event is not bookable'
def test_booking_api_fillslots_slots_string_param(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
)
Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
)
events_ids = [x.id for x in Event.objects.filter(agenda=agenda)]
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})
assert Booking.objects.count() == 2
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() == 4
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']
@pytest.mark.freeze_time('2022-01-20 14:00') # Thursday
def test_booking_api_meeting_weekday_indexes(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60
)
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
desk = Desk.objects.create(agenda=agenda, label='desk')
time_period = TimePeriod.objects.create(
weekday=3, # Thursday
weekday_indexes=[1, 3],
start_time=datetime.time(11, 0),
end_time=datetime.time(12, 0),
desk=desk,
)
datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug))
slot = datetimes_resp.json['data'][0]['id']
assert slot == 'plop:2022-02-03-1100'
app.authorization = ('Basic', ('john.doe', 'password'))
# single booking
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
assert Booking.objects.count() == 1
assert resp.json['duration'] == 30
# multiple slots
slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']]
assert slots == ['plop:2022-02-03-1130', 'plop:2022-02-17-1100']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
assert Booking.objects.count() == 3
# try to book slot on a skipped week
slot = datetimes_resp.json['data'][3]['id']
time_period.weekday_indexes = [1]
time_period.save()
resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug))
assert slot not in {slot['id'] for slot in resp.json['data']}
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'no more desk available'
def test_booking_api_with_data(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
)
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'}
@pytest.mark.freeze_time('2022-10-24 10:00')
def test_booking_api_meeting_date_time_period(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=8
)
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
desk = Desk.objects.create(agenda=agenda, label='desk')
TimePeriod.objects.create(
date=datetime.date(2022, 10, 24),
start_time=datetime.time(12, 0),
end_time=datetime.time(14, 0),
desk=desk,
)
datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug))
slot = datetimes_resp.json['data'][0]['id']
assert slot == 'plop:2022-10-24-1200'
app.authorization = ('Basic', ('john.doe', 'password'))
# single booking
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
assert Booking.objects.count() == 1
assert resp.json['duration'] == 30
# multiple slots
slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']]
assert slots == ['plop:2022-10-24-1230', 'plop:2022-10-24-1300']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
assert Booking.objects.count() == 3
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'no more desk available'
def test_booking_api_available(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0)
events = []
for i in range(0, 10):
events.append(
Event.objects.create(
slug='event-slug%i' % i,
start_datetime=(now() + datetime.timedelta(days=5)).replace(hour=10, minute=i),
places=20,
agenda=agenda,
)
)
event = random.choice(events)
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) == 3
event_data = [d for d in resp.json['data'] if d['id'] == event.slug][0]
assert event_data['places']['total'] == 20
assert event_data['places']['available'] == 20
assert event_data['places']['reserved'] == 0
assert event_data['places']['full'] is False
assert 'waiting_list_total' not in event_data['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)
event_data = [d for d in resp.json['data'] if d['id'] == event.slug][0]
assert event_data['places']['total'] == 20
assert event_data['places']['available'] == 19
assert event_data['places']['reserved'] == 1
assert event_data['places']['full'] is False
assert 'waiting_list_total' not in event_data['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)
event_data = [d for d in resp.json['data'] if d['id'] == event.slug][0]
assert event_data['places']['total'] == 20
assert event_data['places']['available'] == 19
assert event_data['places']['reserved'] == 1
assert event_data['places']['full'] is False
assert event_data['places']['waiting_list_total'] == 5
assert event_data['places']['waiting_list_available'] == 3
assert event_data['places']['waiting_list_reserved'] == 2
assert event_data['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, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=20, agenda=agenda
)
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, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
events = []
for _ in range(3):
events.append(
Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=20, agenda=agenda
)
)
event_0, event_1, event_2 = events[0], events[1], events[2]
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, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=2, agenda=agenda
)
resp = app.get('/api/agenda/%s/datetimes/' % agenda.pk)
assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 1
assert event.slug in [x['id'] for x in resp.json['data']]
for _ in range(2):
Booking.objects.create(event=event)
resp = app.get('/api/agenda/%s/datetimes/' % agenda.pk)
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['id'] == event.slug
assert resp.json['data'][0]['disabled'] is True
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.pk, 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, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=5),
places=2,
waiting_list_places=2,
agenda=agenda,
)
for _ in range(2):
Booking.objects.create(event=event)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.id), status=200)
assert resp.json['err'] == 0
assert resp.json['in_waiting_list'] is True
# 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.pk, event.id), status=200)
assert resp.json['err'] == 0
assert resp.json['in_waiting_list'] is True
# fill the waiting list
for _ in range(2):
Booking.objects.create(event=event, in_waiting_list=True)
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.pk, 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, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event',
start_datetime=now() + datetime.timedelta(days=5),
places=10,
agenda=agenda,
)
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)"
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"
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, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
events = []
for i in range(2):
events.append(
Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5 + i), places=20, agenda=agenda
)
)
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, 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')
time_period = TimePeriod.objects.create(
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk
)
app.authorization = ('Basic', ('john.doe', 'password'))
# add booking of another meeting type
meeting_type2 = MeetingType.objects.create(agenda=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.pk, event_id))
cancel_url = resp.json['api']['cancel_url']
# add a second desk
desk2 = Desk.objects.create(label='Desk 2', agenda=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.pk, 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.pk, 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)
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(ctx.captured_queries) == 9
assert len(resp2.json['data']) == len([x for x in resp.json['data'] if not x['disabled']])
with CaptureQueriesContext(connection) as ctx:
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event_id))
assert len(ctx.captured_queries) == 18
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.pk, 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.pk, 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) == 17
with CaptureQueriesContext(connection) as ctx:
app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(ctx.captured_queries) == 9
def test_agenda_meeting_api_fillslots_multiple_desks(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')
time_period = TimePeriod.objects.create(
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk
)
app.authorization = ('Basic', ('john.doe', 'password'))
# add a second desk, same timeperiods
time_period = agenda.desk_set.first().timeperiod_set.first()
desk2 = Desk.objects.create(label='Desk 2', agenda=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.pk
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.pk, 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, 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']
@pytest.mark.freeze_time('2022-02-05')
def test_virtual_agendas_meetings_booking(app, 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'
@pytest.mark.freeze_time('2022-02-05')
def test_virtual_agendas_meetings_booking_default_policy(app, 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('2022-03-30 08:00') # wednesday
def test_virtual_honor_real_agenda_booking_delay(app, user):
foo_agenda = Agenda.objects.create(
label='Foo', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=1
)
foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Plop', duration=60)
foo_desk = Desk.objects.create(agenda=foo_agenda, label='desk')
bar_agenda = Agenda.objects.create(
label='Foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=2
)
MeetingType.objects.create(agenda=bar_agenda, label='Plop', duration=60)
bar_desk = Desk.objects.create(agenda=bar_agenda, label='desk')
for desk in (foo_desk, bar_desk):
TimePeriod.objects.create(
weekday=2, # wednesday
start_time=datetime.time(11, 0),
end_time=datetime.time(12, 0),
desk=desk,
)
TimePeriod.objects.create(
weekday=3, # thursday
start_time=datetime.time(11, 0),
end_time=datetime.time(12, 0),
desk=desk,
)
virtual_agenda = Agenda.objects.create(
label='Agenda Virtual', kind='virtual', minimal_booking_delay=None, maximal_booking_delay=None
)
virtual_agenda.real_agendas.add(foo_agenda, bar_agenda)
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, foo_meeting_type.slug)
datetimes_resp = app.get(api_url)
assert len(datetimes_resp.json['data']) == 2
assert datetimes_resp.json['data'][0]['datetime'] == '2022-03-30 11:00:00' # comes from foo_agenda
assert datetimes_resp.json['data'][1]['datetime'] == '2022-03-31 11:00:00' # comes from bar_agenda
assert Booking.objects.count() == 0
app.authorization = ('Basic', ('john.doe', 'password'))
# minimal_booking_delay of bar_agenda must be honored
fillslot_url = datetimes_resp.json['data'][0]['api']['fillslot_url']
# book, this must end up in foo_agenda
resp = app.post(fillslot_url)
assert resp.json['err'] == 0
assert Booking.objects.count() == 1
booking = Booking.objects.first()
assert booking.event.agenda == foo_agenda
# try to book again, this must fail
resp = app.post(fillslot_url)
assert resp.json['err'] == 1
booking.delete()
assert Booking.objects.count() == 0
# maximal_booking_delay of foo_agenda must be honored
fillslot_url = datetimes_resp.json['data'][1]['api']['fillslot_url']
# book, this must end up in bar_agenda
resp = app.post(fillslot_url)
assert resp.json['err'] == 0
assert Booking.objects.count() == 1
booking = Booking.objects.first()
assert booking.event.agenda == bar_agenda
# try to book again, this must fail
resp = app.post(fillslot_url)
assert resp.json['err'] == 1
@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()],
recurrence_end_date=now() + datetime.timedelta(days=30),
places=5,
agenda=agenda,
)
event.create_all_recurrences()
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 slugs: %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'}, status=400
)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'invalid slugs: %s' % event_slug
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.objects.get(primary_event=event, start_datetime=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_url_translation(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
assert Booking.objects.count() == 0
resp = app.get('/api/agenda/%s/datetimes/' % agenda.pk)
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'
# monday 15th march at 12:00 CEST
@pytest.mark.freeze_time('2021-03-15T12:00:00+01:00')
def test_user_external_id(app, user):
user_external_id = 'foobar'
meeting_agenda_1 = Agenda.objects.create(
label='Meeting agenda', slug='mt1', kind='meetings', minimal_booking_delay=0
)
desk = Desk.objects.create(agenda=meeting_agenda_1, slug='desk')
meeting_type = MeetingType.objects.create(agenda=meeting_agenda_1, slug='mtt')
TimePeriod.objects.create(
weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(17, 00), desk=desk
)
meeting_agenda_2 = meeting_agenda_1.duplicate()
meeting_agenda_2.maximal_booking_delay = 20
meeting_agenda_2.save()
meeting_agenda_3 = meeting_agenda_1.duplicate()
meeting_agenda_3.maximal_booking_delay = 30
meeting_agenda_3.save()
virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
virtual_agenda.real_agendas.add(meeting_agenda_1, meeting_agenda_2, meeting_agenda_3)
meeting_agenda_1_datetimes_url = f'/api/agenda/{meeting_agenda_1.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
meeting_agenda_2_datetimes_url = f'/api/agenda/{meeting_agenda_2.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
meeting_agenda_3_datetimes_url = f'/api/agenda/{meeting_agenda_3.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
virtual_agenda_datetimes_url = f'/api/agenda/{virtual_agenda.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
today_at_15 = localtime(now()).replace(hour=15, minute=0)
# check all slots are free
resp = app.get(virtual_agenda_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
resp = app.get(meeting_agenda_1_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
resp = app.get(meeting_agenda_2_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
resp = app.get(meeting_agenda_3_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
for agenda in virtual_agenda.get_real_agendas():
meeting_event = Event.objects.create(
label='Event',
slug='meeting1',
start_datetime=today_at_15,
agenda=agenda,
places=1,
meeting_type=agenda.meetingtype_set.all()[0],
desk=agenda.desk_set.all()[0],
)
Booking.objects.create(event=meeting_event, user_external_id=user_external_id)
# now some slots are disabled
resp = app.get(virtual_agenda_datetimes_url)
assert any(x['disabled'] for x in resp.json['data'])
agenda_datetimes_url = f'/api/agenda/{agenda.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
resp = app.get(agenda_datetimes_url)
assert any(x['disabled'] for x in resp.json['data'])
for other_agenda in virtual_agenda.get_real_agendas():
if other_agenda == agenda:
continue
other_agenda_datetimes_url = f'/api/agenda/{other_agenda.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
resp = app.get(other_agenda_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
meeting_event.delete()