import datetime import urllib.parse as urlparse import pytest import sys from django.contrib.auth import get_user_model from django.db import connection from django.test import override_settings from django.test.utils import CaptureQueriesContext from django.utils.timezone import now, make_aware, localtime from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk, TimePeriodException import chrono.api.views pytestmark = pytest.mark.django_db def datetime_from_str(dt_str): return datetime.datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') @pytest.fixture def user(): User = get_user_model() user = User.objects.create( username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net' ) user.set_password('password') user.save() return user @pytest.fixture(params=['Europe/Brussels', 'Asia/Kolkata', 'Brazil/East']) def time_zone(request, settings): settings.TIME_ZONE = request.param @pytest.fixture( params=[ datetime.datetime(2017, 5, 20, 1, 12), datetime.datetime(2017, 5, 20, 11, 42), datetime.datetime(2017, 5, 20, 23, 17), ] ) def mock_now(request, monkeypatch): def mockreturn(): return make_aware(request.param) monkeypatch.setattr(chrono.api.views, 'now', mockreturn) monkeypatch.setattr(chrono.agendas.models, 'now', mockreturn) monkeypatch.setattr(sys.modules[__name__], 'now', mockreturn) return mockreturn() @pytest.fixture def some_data(time_zone, mock_now): agenda = Agenda(label=u'Foo bar') agenda.save() first_date = localtime(now()).replace(hour=17, minute=0, second=0, microsecond=0) first_date += datetime.timedelta(days=1) for i in range(3): event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda) event.save() agenda2 = Agenda(label=u'Foo bar2') agenda2.save() first_date = localtime(now()).replace(hour=20, minute=0, second=0, microsecond=0) first_date += datetime.timedelta(days=1) for i in range(2): event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda2) event.save() # a date in the past event = Event(start_datetime=first_date - datetime.timedelta(days=10), places=10, agenda=agenda) event.save() @pytest.fixture def meetings_agenda(time_zone, mock_now): agenda = Agenda( label=u'Foo bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56 ) agenda.save() meeting_type = MeetingType(agenda=agenda, label='Blah', duration=30) meeting_type.save() test_1st_weekday = (localtime(now()).weekday() + 2) % 7 test_2nd_weekday = (localtime(now()).weekday() + 3) % 7 default_desk, created = Desk.objects.get_or_create(agenda=agenda, label='Desk 1') time_period = TimePeriod( weekday=test_1st_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk, ) time_period.save() time_period = TimePeriod( weekday=test_2nd_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(17, 0), desk=default_desk, ) time_period.save() return agenda def test_agendas_api(app, some_data, meetings_agenda): agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] agenda2 = Agenda.objects.filter(label=u'Foo bar2')[0] resp = app.get('/api/agenda/') assert resp.json == { 'data': [ { 'text': 'Foo bar', 'id': u'foo-bar', 'slug': 'foo-bar', 'kind': 'events', 'minimal_booking_delay': 1, 'maximal_booking_delay': 56, 'api': { 'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda1.slug, 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda1.slug, }, }, { 'text': 'Foo bar Meeting', 'id': u'foo-bar-meeting', 'slug': 'foo-bar-meeting', 'minimal_booking_delay': 1, 'maximal_booking_delay': 56, 'kind': 'meetings', 'api': { 'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % meetings_agenda.slug, 'desks_url': 'http://testserver/api/agenda/%s/desks/' % meetings_agenda.slug, 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug, }, }, { 'text': 'Foo bar2', 'id': u'foo-bar2', 'kind': 'events', 'slug': 'foo-bar2', 'minimal_booking_delay': 1, 'maximal_booking_delay': 56, 'api': { 'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda2.slug, 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug, }, }, ] } def test_agendas_meetingtypes_api(app, some_data, meetings_agenda): resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug) assert resp.json == { 'data': [ { 'text': 'Blah', 'id': 'blah', 'duration': 30, 'api': { 'datetimes_url': 'http://testserver/api/agenda/foo-bar-meeting/meetings/blah/datetimes/', }, } ] } # wrong kind agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] resp = app.get('/api/agenda/%s/meetings/' % agenda1.slug, status=404) # unknown resp = app.get('/api/agenda/xxxx/meetings/', status=404) def test_agendas_desks_api(app, some_data, meetings_agenda): resp = app.get('/api/agenda/%s/desks/' % meetings_agenda.slug) assert resp.json == {'data': [{'text': 'Desk 1', 'id': 'desk-1',}]} # wrong kind agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] resp = app.get('/api/agenda/%s/desks/' % agenda1.slug, status=404) # unknown resp = app.get('/api/agenda/xxxx/desks/', status=404) def test_datetimes_api(app, some_data): agenda = Agenda.objects.filter(label=u'Foo bar')[0] def check_bookability(data): for event in data: assert Event.objects.get(id=event['id']).in_bookable_period() for event in agenda.event_set.all(): if not event.in_bookable_period(): assert not event.id in [x['id'] for x in data] resp = app.get('/api/agenda/xxx/datetimes/', status=404) resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) assert 'data' in resp.json assert len(resp.json['data']) == 3 check_bookability(resp.json['data']) assert app.get('/api/agenda/%s/datetimes/' % agenda.id).json == resp.json agenda.minimal_booking_delay = 5 agenda.save() resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) assert len(resp.json['data']) == 0 check_bookability(resp.json['data']) agenda.minimal_booking_delay = 2 agenda.save() resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) assert len(resp.json['data']) == 2 check_bookability(resp.json['data']) agenda.minimal_booking_delay = 0 agenda.maximal_booking_delay = 3 agenda.save() resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) assert len(resp.json['data']) == 2 check_bookability(resp.json['data']) assert resp.json['data'][0]['description'] is None # add description to events for i, event in enumerate(agenda.event_set.all()): event.description = 'Description %s' % i event.save() resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) assert resp.json['data'][0]['description'] def test_datetimes_api_wrong_kind(app, some_data): agenda = Agenda.objects.filter(label=u'Foo bar')[0] agenda.kind = 'meetings' agenda.save() resp = app.get('/api/agenda/%s/datetimes/' % agenda.id, status=404) def test_datetime_api_fr(app, some_data): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id with override_settings(LANGUAGE_CODE='fr-fr'): resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) # no seconds, hh:mm in 24-hour formats assert resp.json['data'][0]['text'].endswith(' 17:00') assert resp.json['data'][0]['datetime'].endswith(' 17:00:00') assert 'data' in resp.json def test_datetime_api_label(app, some_data): agenda_id = Agenda.objects.filter(label=u'Foo bar2')[0].id event = Event.objects.filter(agenda=agenda_id)[0] event.label = 'Hello world' event.save() resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) assert 'Hello world' in [x['text'] for x in resp.json['data']] def test_datetime_api_status_url(app, some_data): agenda = Agenda.objects.get(label=u'Foo bar2') resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) for datum in resp.json['data']: assert urlparse.urlparse(datum['api']['status_url']).path == '/api/agenda/%s/status/%s/' % ( agenda.slug, datum['slug'] or datum['id'], ) def test_datetimes_api_meetings_agenda(app, meetings_agenda): meeting_type = MeetingType.objects.get(agenda=meetings_agenda) api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug) resp = app.get('/api/agenda/%s/meetings/xxx/datetimes/' % meeting_type.agenda.slug, status=404) resp = app.get(api_url) assert len(resp.json['data']) == 144 assert resp.json == app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id).json meetings_agenda.minimal_booking_delay = 7 meetings_agenda.maximal_booking_delay = 28 meetings_agenda.save() resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == 54 meetings_agenda.minimal_booking_delay = 1 meetings_agenda.maximal_booking_delay = 56 meetings_agenda.save() resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == 144 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') ev = Event( agenda=meetings_agenda, meeting_type=meeting_type, places=1, full=False, start_datetime=make_aware(dt), desk=Desk.objects.first(), ) ev.save() booking = Booking(event=ev) booking.save() resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp2.json['data']) == 144 assert resp.json['data'][0] == resp2.json['data'][0] assert resp.json['data'][1] == resp2.json['data'][1] assert resp.json['data'][2] != resp2.json['data'][2] assert resp.json['data'][2]['disabled'] is False assert resp2.json['data'][2]['disabled'] is True assert resp.json['data'][3] == resp2.json['data'][3] # test with a timeperiod overlapping current moment, it should get one # datetime for the current timeperiod + two from the next week. if now().time().hour == 23: # skip this part of the test as it would require support for events # crossing midnight return default_desk, _ = Desk.objects.get_or_create(agenda=meetings_agenda, slug='desk-1') TimePeriod.objects.filter(desk=default_desk).delete() start_time = localtime(now()) - datetime.timedelta(minutes=10) time_period = TimePeriod( weekday=localtime(now()).weekday(), start_time=start_time, end_time=start_time + datetime.timedelta(hours=1), desk=default_desk, ) time_period.save() meetings_agenda.minimal_booking_delay = 0 meetings_agenda.maximal_booking_delay = 10 meetings_agenda.save() resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == 3 def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda, user): meetings_agenda.minimal_booking_delay = 0 meetings_agenda.maximal_booking_delay = 10 meetings_agenda.save() default_desk, _ = Desk.objects.get_or_create(agenda=meetings_agenda, slug='desk-1') meeting_type = MeetingType.objects.get(agenda=meetings_agenda) api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug) # test with short time periods TimePeriod.objects.filter(desk=default_desk).delete() test_1st_weekday = (localtime(now()).weekday() + 2) % 7 time_period = TimePeriod( weekday=test_1st_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(10, 30), desk=default_desk, ) time_period.save() resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == 2 fillslot_url = resp.json['data'][0]['api']['fillslot_url'] two_slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']] time_period.end_time = datetime.time(10, 15) time_period.save() resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == 0 # check booking is not possible app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post(fillslot_url) 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' # booking the two slots fails too fillslots_url = '/api/agenda/%s/fillslots/' % meeting_type.agenda.slug resp = app.post(fillslots_url, params={'slots': two_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' def test_booking_api(app, some_data, user): agenda = Agenda.objects.filter(label=u'Foo bar')[0] event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] # unauthenticated resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id), status=401) for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy) resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key) event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api'][ 'fillslot_url' ] assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % ( agenda.slug, event.slug or event.id, ) 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 'accept_url' not in resp.json['api'] assert 'cancel_url' in resp.json['api'] assert 'ics_url' in resp.json['api'] assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc assert urlparse.urlparse(resp.json['api']['ics_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/'}, ) assert Booking.objects.get(id=resp.json['booking_id']).label == 'foo' assert Booking.objects.get(id=resp.json['booking_id']).user_name == 'bar' assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == 'http://example.net/' # blank data are OK resp = app.post_json( '/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), params={'label': '', 'user_name': '', 'backoffice_url': ''}, ) assert Booking.objects.get(id=resp.json['booking_id']).label == '' assert Booking.objects.get(id=resp.json['booking_id']).user_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_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_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'} # test invalid data are refused resp = app.post_json( '/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), params={'user_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_name' in resp.json['errors'] resp = app.post('/api/agenda/foobar/fillslot/%s/' % event.id, status=404) resp = app.post('/api/agenda/233/fillslot/%s/' % event.id, status=404) def test_booking_ics(app, some_data, meetings_agenda, user): agenda = Agenda.objects.filter(label=u'Foo bar')[0] event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id)) assert Booking.objects.count() == 1 assert 'ics_url' in resp.json['api'] assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc assert urlparse.urlparse(resp.json['api']['ics_url']).netloc formatted_start_date = event.start_datetime.strftime('%Y%m%dT%H%M%S') booking_ics = Booking.objects.get(id=resp.json['booking_id']).get_ics() assert ( 'UID:%s-%s-%s\r\n' % (event.start_datetime.isoformat(), agenda.pk, resp.json['booking_id']) in booking_ics ) assert 'SUMMARY:\r\n' in booking_ics assert 'DTSTART:%sZ\r\n' % formatted_start_date in booking_ics assert 'DTEDND:' not in booking_ics # 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/', 'url': 'http://example.com/booking', }, ) assert Booking.objects.count() == 2 booking_ics = Booking.objects.get(id=resp.json['booking_id']).get_ics() assert 'SUMMARY:foo\r\n' in booking_ics assert 'ATTENDEE:bar\r\n' in booking_ics assert 'URL:http://example.com/booking\r\n' in booking_ics # test with user_label in additionnal 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/', 'url': 'http://example.com/booking', 'user_display_label': 'your booking', }, ) assert Booking.objects.count() == 3 booking_ics = Booking.objects.get(id=resp.json['booking_id']).get_ics() assert 'SUMMARY:your booking\r\n' in booking_ics assert 'ATTENDEE:bar\r\n' in booking_ics assert 'URL:http://example.com/booking\r\n' in booking_ics # extra data stored in extra_data field resp = app.post_json( '/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), params={ 'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'location': 'bar', 'comment': 'booking comment', 'description': 'booking description', }, ) assert Booking.objects.count() == 4 booking_id = resp.json['booking_id'] booking = Booking.objects.get(id=booking_id) booking_ics = booking.get_ics() assert 'COMMENT:booking comment\r\n' in booking_ics assert 'LOCATION:bar\r\n' in booking_ics assert 'DESCRIPTION:booking description\r\n' in booking_ics # unauthenticated app.authorization = None app.get('/api/booking/%s/ics/' % resp.json['booking_id'], status=401) app.authorization = ('Basic', ('john.doe', 'password')) resp = app.get('/api/booking/%s/ics/' % resp.json['booking_id']) assert resp.headers['Content-Type'] == 'text/calendar' params = { 'description': 'custom booking description', 'location': 'custom booking location', 'comment': 'custom comment', 'url': 'http://example.com/custom', } resp = app.get('/api/booking/%s/ics/' % booking_id, params=params) assert 'DESCRIPTION:custom booking description\r\n' in resp.text assert 'LOCATION:custom booking location\r\n' in resp.text assert 'COMMENT:custom comment\r\n' in resp.text assert 'URL:http://example.com/custom\r\n' in resp.text meetings_agenda_id = Agenda.objects.filter(label=u'Foo bar Meeting')[0].id meeting_type = MeetingType.objects.get(agenda=meetings_agenda) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) event = resp.json['data'][2] resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda_id, event['id'])) assert Booking.objects.count() == 5 assert 'ics_url' in resp.json['api'] booking = Booking.objects.get(id=resp.json['booking_id']) booking_ics = booking.get_ics() start = booking.event.start_datetime.strftime('%Y%m%dT%H%M%S') end = ( booking.event.start_datetime + datetime.timedelta(minutes=booking.event.meeting_type.duration) ).strftime('%Y%m%dT%H%M%S') assert "DTSTART:%sZ\r\n" % start in booking_ics assert "DTEND:%sZ\r\n" % end in booking_ics def test_booking_api_fillslots(app, some_data, user): agenda = Agenda.objects.filter(label=u'Foo bar')[0] events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()] for i, event in enumerate([x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()]): event.slug = 'event-%s' % i event.save() events_slugs = [x.slug for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()] assert len(events_ids) == 3 event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] # first event # unauthenticated resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, status=401) for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy) resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key) api_event_ids = [x['id'] for x in resp_datetimes.json['data']] assert api_event_ids == events_ids 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 'accept_url' not in resp.json['api'] assert 'cancel_url' in resp.json['api'] 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('primary_booking') 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_name': 'bar', 'backoffice_url': 'http://example.net/', }, ) booking_id = resp.json['booking_id'] assert Booking.objects.get(id=booking_id).label == 'foo' assert Booking.objects.get(id=booking_id).user_name == 'bar' assert Booking.objects.get(id=booking_id).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_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_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_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_name' in resp.json['errors'] # empty or missing slots resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': []}, status=400) assert resp.json['err'] == 1 assert resp.json['reason'] == 'slots list cannot be empty' # legacy assert resp.json['err_class'] == 'slots list cannot be empty' assert resp.json['err_desc'] == 'slots list cannot be empty' resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, status=400) assert resp.json['err'] == 1 assert resp.json['reason'] == 'slots list cannot be empty' # legacy assert resp.json['err_class'] == 'slots list cannot be empty' assert resp.json['err_desc'] == 'slots list cannot be empty' # 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 payload' # legacy assert resp.json['err_class'] == 'invalid payload' assert resp.json['err_desc'] == 'invalid payload' assert len(resp.json['errors']) == 1 assert 'slots' in resp.json['errors'] # unknown agendas resp = app.post('/api/agenda/foobar/fillslots/', status=404) resp = app.post('/api/agenda/233/fillslots/', status=404) def test_booking_api_fillslots_slots_string_param(app, some_data, user): agenda = Agenda.objects.filter(label=u'Foo bar')[0] events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()] assert len(events_ids) == 3 app.authorization = ('Basic', ('john.doe', 'password')) # empty string resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': ''}, status=400) assert resp.json['err'] == 1 assert resp.json['err_class'] == 'invalid payload' assert resp.json['err_desc'] == 'invalid payload' slots_string_param = ','.join([str(e) for e in events_ids]) resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots_string_param}) primary_booking_id = resp.json['booking_id'] Booking.objects.get(id=primary_booking_id) assert Booking.objects.count() == 3 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_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 impossible_slots = ['1:2017-05-22-1130', '2:2017-05-22-1100'] resp = app.post('/api/agenda/%s/fillslots/' % agenda_id, 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)' def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user): meetings_agenda.maximal_booking_delay = 365 meetings_agenda.save() agenda_id = meetings_agenda.slug meeting_type = MeetingType.objects.get(agenda=meetings_agenda) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) event_index = 26 * 18 event_id = resp.json['data'][event_index]['id'] assert event_id[-4:] == resp.json['data'][2 * 18]['id'][-4:] assert urlparse.urlparse( resp.json['data'][event_index]['api']['fillslot_url'] ).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id) app.authorization = ('Basic', ('john.doe', 'password')) resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 1 assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime).strftime( '%Y-%m-%d %H:%M:%S' ) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert resp.json['data'][event_index]['disabled'] def test_booking_api_meeting_different_durations_book_short(app, meetings_agenda, user): agenda_id = meetings_agenda.id meeting_type = MeetingType.objects.get(agenda=meetings_agenda) meeting_type_2 = MeetingType(agenda=meetings_agenda, label='Shorter', duration=15) meeting_type_2.save() # get long events resp_long = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) # book a short event resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_2.id) event_id = resp.json['data'][0]['id'] app.authorization = ('Basic', ('john.doe', 'password')) app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 1 # the longer event at the same time shouldn't be available anymore resp_long2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert ( len(resp_long.json['data']) == len([x for x in resp_long2.json['data'] if not x.get('disabled')]) + 1 ) assert resp_long.json['data'][1:] == [x for x in resp_long2.json['data'] if not x.get('disabled')] def test_booking_api_meeting_different_durations_book_long(app, meetings_agenda, user): agenda_id = meetings_agenda.id meeting_type = MeetingType.objects.get(agenda=meetings_agenda) meeting_type_2 = MeetingType(agenda=meetings_agenda, label='Shorter', duration=15) meeting_type_2.save() # get short events resp_short = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_2.id) # book a long event resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) event_id = resp.json['data'][0]['id'] app.authorization = ('Basic', ('john.doe', 'password')) app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 1 # this should have removed two short events resp_short2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_2.id) assert ( len(resp_short.json['data']) == len([x for x in resp_short2.json['data'] if not x.get('disabled')]) + 2 ) # book another long event event_id = resp.json['data'][10]['id'] app.authorization = ('Basic', ('john.doe', 'password')) app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 2 resp_short2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_2.id) assert ( len(resp_short.json['data']) == len([x for x in resp_short2.json['data'] if not x.get('disabled')]) + 4 ) def test_booking_api_with_data(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id)[0] app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), params={'hello': 'world'}) assert Booking.objects.count() == 1 assert Booking.objects.all()[0].extra_data == {'hello': 'world'} def test_booking_api_available(app, some_data, meetings_agenda, user): agenda = Agenda.objects.filter(label=u'Foo bar')[0] event = Event.objects.filter(agenda=agenda)[0] app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.pk, event.pk)) assert resp.json['err'] == 0 assert resp.json['places']['total'] == 10 assert resp.json['places']['available'] == 9 assert resp.json['places']['reserved'] == 1 assert 'waiting_list_total' not in resp.json['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'] == 10 assert resp.json['places']['available'] == 9 assert resp.json['places']['reserved'] == 1 assert resp.json['places']['waiting_list_total'] == 5 assert resp.json['places']['waiting_list_available'] == 3 assert resp.json['places']['waiting_list_reserved'] == 2 # not for mettings agenda meeting_type = MeetingType.objects.get(agenda=meetings_agenda) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.pk) event_id = resp.json['data'][2]['id'] resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.pk, event_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_with_cancel_booking(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event_0, event_1, event_2, event_3 = Event.objects.filter(agenda_id=agenda_id)[0:4] app.authorization = ('Basic', ('john.doe', 'password')) app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id)) assert Booking.objects.count() == 1 first_booking = Booking.objects.first() # Book a new event and cancel previous booking resp = app.post_json( '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), params={'cancel_booking_id': first_booking.pk}, ) assert resp.json['err'] == 0 assert resp.json['cancelled_booking_id'] == first_booking.pk assert Booking.objects.count() == 2 first_booking = Booking.objects.get(pk=first_booking.pk) assert first_booking.cancellation_datetime # Cancelling an already cancelled booking returns an error resp = app.post_json( '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), params={'cancel_booking_id': first_booking.pk}, ) assert resp.json['err'] == 1 assert resp.json['reason'] == 'cancel booking: booking already cancelled' # legacy assert resp.json['err_class'] == 'cancel booking: booking already cancelled' assert resp.json['err_desc'] == 'cancel booking: booking already cancelled' assert Booking.objects.count() == 2 # Cancelling a non existent booking returns an error resp = app.post_json( '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), params={'cancel_booking_id': '-1'} ) assert resp.json['err'] == 1 assert resp.json['reason'] == 'cancel booking: booking does no exist' # legacy assert resp.json['err_class'] == 'cancel booking: booking does no exist' assert resp.json['err_desc'] == 'cancel booking: booking does no exist' assert Booking.objects.count() == 2 # Cancelling booking with different count than new booking resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_2.id), params={'count': 2}) assert resp.json['err'] == 0 assert Booking.objects.count() == 4 booking_id = resp.json['booking_id'] resp = app.post_json( '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_3.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_booking_cancellation_api(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id)[0] resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=401) app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id)) booking_id = resp.json['booking_id'] assert Booking.objects.count() == 1 resp = app.delete('/api/booking/%s/' % booking_id) assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 1 def test_booking_cancellation_post_api(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id)[0] resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=401) app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id)) booking_id = resp.json['booking_id'] assert Booking.objects.count() == 1 assert urlparse.urlparse(resp.json['api']['cancel_url']).path == '/api/booking/%s/cancel/' % booking_id resp = app.post('/api/booking/%s/cancel/' % booking_id) assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 1 # cancel an object that doesn't exist resp = app.post('/api/booking/%s/cancel/' % 9999, status=404) # cancel an event that was already cancelled resp = app.post('/api/booking/%s/cancel/' % booking_id, status=200) assert resp.json['err'] == 1 def test_booking_cancellation_post_meeting_api(app, meetings_agenda, user): agenda_id = Agenda.objects.filter(label=u'Foo bar Meeting')[0].id meeting_type = MeetingType.objects.get(agenda=meetings_agenda) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) nb_events = len(resp.json['data']) event_id = resp.json['data'][2]['id'] app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 1 booking_id = resp.json['booking_id'] assert Booking.objects.count() == 1 resp = app.post('/api/booking/%s/cancel/' % booking_id) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == nb_events # book the same time slot resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert resp.json['err'] == 0 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len([x for x in resp.json['data'] if not x.get('disabled')]) == nb_events - 1 def test_soldout(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0] resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 3 assert event.id in [x['id'] for x in resp.json['data']] for i in range(event.places): Booking(event=event).save() resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 2 assert not event.id in [x['id'] for x in resp.json['data'] if not x.get('disabled')] assert event.id in [x['id'] for x in resp.json['data'] if x.get('disabled')] app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200) assert resp.json['err'] == 1 assert resp.json['reason'] == 'sold out' # legacy assert resp.json['err_class'] == 'sold out' assert resp.json['err_desc'] == 'sold out' def test_status(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id)[0] Booking(event=event).save() resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, event.id), status=401) app.authorization = ('Basic', ('john.doe', 'password')) resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, 9999), status=404) resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, 'xx'), status=404) resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, event.id)) assert resp.json['err'] == 0 assert resp.json['places']['total'] == 10 assert resp.json['places']['available'] == 9 assert resp.json['places']['reserved'] == 1 assert not 'waiting_list_total' in resp.json['places'] Booking(event=event, in_waiting_list=True).save() event.waiting_list_places = 5 event.save() resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, event.id)) assert resp.json['places']['waiting_list_total'] == 5 assert resp.json['places']['waiting_list_available'] == 4 assert resp.json['places']['waiting_list_reserved'] == 1 # access by slug event.slug = 'bar' event.save() resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, event.slug)) # not found event resp = app.get('/api/agenda/%s/status/%s/' % (agenda_id, 'unknown'), status=404) def test_waiting_list_datetimes(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0] event.waiting_list_places = 5 event.save() resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 3 assert event.id in [x['id'] for x in resp.json['data']] for i in range(event.places): Booking(event=event).save() # all places are booked but all the dates are still displayed as there is a # waiting list. resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 3 # fill the waiting list for i in range(event.waiting_list_places): Booking(event=event, in_waiting_list=True).save() # the event datetime should no longer be returned resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 2 assert not event.id in [x['id'] for x in resp.json['data'] if not x.get('disabled')] assert event.id in [x['id'] for x in resp.json['data'] if x.get('disabled')] def test_waiting_list_booking(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0] event.waiting_list_places = 5 event.save() for i in range(event.places): Booking(event=event).save() app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200) assert resp.json['err'] == 0 assert resp.json['in_waiting_list'] is True assert 'accept_url' in resp.json['api'] assert 'cancel_url' in resp.json['api'] assert 'ics_url' in resp.json['api'] assert urlparse.urlparse(resp.json['api']['accept_url']).netloc assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc assert urlparse.urlparse(resp.json['api']['ics_url']).netloc # cancel a booking that was not on the waiting list booking = Booking.objects.filter(event=event, in_waiting_list=False)[0] booking.cancel() # check new booking is still done on the waiting list app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200) assert resp.json['err'] == 0 assert resp.json['in_waiting_list'] is True # fill the waiting list for i in range(event.waiting_list_places): Booking(event=event, in_waiting_list=True).save() app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200) assert resp.json['err'] == 1 assert resp.json['reason'] == 'sold out' # legacy assert resp.json['err_class'] == 'sold out' assert resp.json['err_desc'] == 'sold out' def test_accept_booking(app, some_data, user): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0] event.waiting_list_places = 5 event.save() # create a booking on the waiting list booking = Booking(event=event, in_waiting_list=True) booking.save() assert Booking.objects.filter(in_waiting_list=True).count() == 1 app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/booking/%s/accept/' % booking.id) assert Booking.objects.filter(in_waiting_list=True).count() == 0 assert Booking.objects.filter(in_waiting_list=False).count() == 1 # accept a booking that doesn't exist resp = app.post('/api/booking/%s/accept/' % 9999, status=404) # accept a booking that was not in the waiting list resp = app.post('/api/booking/%s/accept/' % booking.id, status=200) assert resp.json['err'] == 2 # accept a booking that was cancelled before booking = Booking.objects.get(id=booking.id) booking.in_waiting_list = True booking.cancel() resp = app.post('/api/booking/%s/accept/' % booking.id, status=200) assert resp.json['err'] == 1 assert Booking.objects.filter(in_waiting_list=True).count() == 1 assert Booking.objects.filter(in_waiting_list=False).count() == 0 def test_multiple_booking_api(app, some_data, user): agenda = Agenda.objects.filter(label=u'Foo bar')[0] event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id) event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api'][ 'fillslot_url' ] app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/?count=NaN' % (agenda.slug, event.id), status=400) assert resp.json['err'] == 1 assert resp.json['reason'] == "invalid value for count (NaN)" # legacy assert resp.json['err_class'] == "invalid value for count (NaN)" assert resp.json['err_desc'] == "invalid value for count (NaN)" app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/?count=0' % (agenda.slug, event.id), status=400) assert resp.json['err'] == 1 assert resp.json['reason'] == "count cannot be less than or equal to zero" # legacy assert resp.json['err_class'] == "count cannot be less than or equal to zero" assert resp.json['err_desc'] == "count cannot be less than or equal to zero" app.authorization = ('Basic', ('john.doe', 'password')) resp = app.post('/api/agenda/%s/fillslot/%s/?count=-3' % (agenda.slug, event.id), status=400) assert resp.json['err'] == 1 assert resp.json['reason'] == "count cannot be less than or equal to zero" # legacy assert resp.json['err_class'] == "count cannot be less than or equal to zero" assert resp.json['err_desc'] == "count cannot be less than or equal to zero" resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id)) Booking.objects.get(id=resp.json['booking_id']) assert resp.json['datetime'] == localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S') assert 'accept_url' not in resp.json['api'] assert 'cancel_url' in resp.json['api'] assert Event.objects.get(id=event.id).booked_places == 3 resp2 = 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).waiting_list == 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).waiting_list == 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).waiting_list == 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).waiting_list == 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).waiting_list == 2 def test_multiple_booking_api_fillslots(app, some_data, user): agenda = Agenda.objects.filter(label=u'Foo bar')[0] # get slots of first 2 events events = [ x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period() ][:2] events_ids = [x.id 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_ids] 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' not 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).waiting_list == 5 accept_url = resp.json['api']['accept_url'] return 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).waiting_list == 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).waiting_list == 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).waiting_list == 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).waiting_list == 2 def test_agenda_detail_api(app, some_data): agenda = Agenda.objects.get(slug='foo-bar') resp = app.get('/api/agenda/%s/' % agenda.slug) data = resp.json['data'] assert data['id'] == 'foo-bar' assert data['slug'] == 'foo-bar' assert data['text'] == 'Foo bar' assert data['kind'] == 'events' assert data['api']['datetimes_url'] == 'http://testserver/api/agenda/foo-bar/datetimes/' # unknown app.get('/api/agenda/whatever/', status=404) def test_agenda_api_date_range(app, some_data): # test range limitation agenda2 = Agenda.objects.get(slug='foo-bar2') base_date = agenda2.event_set.last().start_datetime.date() base_date = base_date + datetime.timedelta(days=1) for idx in range(7, 10): if idx == 7: day_events = ['9:00', '10:00', '11:00'] elif idx == 8: day_events = ['13:00', '14:00'] else: day_events = ['8:00'] day = base_date + datetime.timedelta(days=idx) for event in day_events: event_dt = datetime.datetime.combine(day, datetime.datetime.strptime(event, '%H:%M').time()) Event.objects.create(agenda=agenda2, start_datetime=make_aware(event_dt), places=2) params = {'date_start': base_date.isoformat()} resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params) assert len(resp.json['data']) == 6 date_end = base_date + datetime.timedelta(days=7) params = {'date_end': date_end.isoformat()} resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params) assert len(resp.json['data']) == 2 assert resp.json['data'][0]['datetime'] == '2017-05-21 20:00:00' assert resp.json['data'][-1]['datetime'] == '2017-05-22 20:00:00' params = { 'date_start': base_date + datetime.timedelta(days=8), 'date_end': base_date + datetime.timedelta(days=10), } resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params) assert len(resp.json['data']) == 3 assert resp.json['data'][0]['datetime'] == '2017-05-31 13:00:00' assert resp.json['data'][-1]['datetime'] == '2017-06-01 08:00:00' # with minimal booking delay changed agenda2.minimal_booking_delay = 3 agenda2.save() params = {'date_start': '2017-05-21'} resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params) assert len(resp.json['data']) == 6 assert resp.json['data'][0]['datetime'] == '2017-05-30 09:00:00' assert resp.json['data'][-1]['datetime'] == '2017-06-01 08:00:00' # with maximal booking delay changed agenda2.maximal_booking_delay = 11 agenda2.save() params = {'date_end': '2017-06-01'} resp = app.get('/api/agenda/%s/datetimes/' % agenda2.slug, params=params) assert len(resp.json['data']) == 3 assert resp.json['data'][0]['datetime'] == '2017-05-30 09:00:00' assert resp.json['data'][-1]['datetime'] == '2017-05-30 11:00:00' def test_agenda_meeting_api_multiple_desk(app, meetings_agenda, user): app.authorization = ('Basic', ('john.doe', 'password')) agenda_id = meetings_agenda.slug meeting_type = MeetingType.objects.get(agenda=meetings_agenda) # add booking of another meeting type meeting_type2 = MeetingType.objects.create(agenda=meetings_agenda, label='Tux kart', duration=60) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type2.id) event_id = resp.json['data'][0]['id'] resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) cancel_url = resp.json['api']['cancel_url'] # add a second desk time_period = meetings_agenda.desk_set.first().timeperiod_set.first() desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) TimePeriod.objects.create( start_time=time_period.start_time, end_time=time_period.end_time, weekday=time_period.weekday, desk=desk2, ) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) event_id = resp.json['data'][1]['id'] resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 2 assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime( '%Y-%m-%d %H:%M:%S' ) resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x['disabled']]) + 1 # try booking the same timeslot and fail resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 2 assert resp.json['err'] == 1 assert resp.json['reason'] == 'no more desk available' # legacy assert resp.json['err_class'] == 'no more desk available' assert resp.json['err_desc'] == 'no more desk available' # cancel first booking and retry resp = app.post(cancel_url) # capture number of queries made for datetime endpoint with few bookings with CaptureQueriesContext(connection) as ctx: resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) queries_count_datetime1 = len(ctx.captured_queries) assert len(resp2.json['data']) == len([x for x in resp.json['data'] if not x['disabled']]) # capture number of queries made for fillslot endpoint with few bookings with CaptureQueriesContext(connection) as ctx: resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) queries_count_fillslot1 = len(ctx.captured_queries) assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime( '%Y-%m-%d %H:%M:%S' ) cancel_url = resp.json['api']['cancel_url'] resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp2.json['data']) == len([x for x in resp3.json['data'] if not x['disabled']]) + 1 # cancel a booking resp = app.post(cancel_url) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len(resp2.json['data']) # try booking the same slot to make sure that cancelled booking has freed the slot resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert Booking.objects.count() == 4 assert Booking.objects.exclude(cancellation_datetime__isnull=True).count() == 2 assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime( '%Y-%m-%d %H:%M:%S' ) # try booking the same timeslot again and fail resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert resp.json['err'] == 1 assert resp.json['reason'] == 'no more desk available' # legacy assert resp.json['err_class'] == 'no more desk available' assert resp.json['err_desc'] == 'no more desk available' # fill the agenda and make sure big O is O(1) for idx, event_data in enumerate(resp2.json['data'][2:10]): booking_url = event_data['api']['fillslot_url'] with CaptureQueriesContext(connection) as ctx: app.post(booking_url) assert len(ctx.captured_queries) == queries_count_fillslot1 with CaptureQueriesContext(connection) as ctx: app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert queries_count_datetime1 == len(ctx.captured_queries) def test_agenda_meeting_api_fillslots_multiple_desks(app, meetings_agenda, user): app.authorization = ('Basic', ('john.doe', 'password')) agenda_id = meetings_agenda.slug meeting_type = MeetingType.objects.get(agenda=meetings_agenda) # add a second desk, same timeperiods time_period = meetings_agenda.desk_set.first().timeperiod_set.first() desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) TimePeriod.objects.create( start_time=time_period.start_time, end_time=time_period.end_time, weekday=time_period.weekday, desk=desk2, ) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) slots = [x['id'] for x in resp.json['data'][:3]] def get_free_places(): resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) return len([x for x in resp.json['data'] if not x['disabled']]) start_free_places = get_free_places() # booking 3 slots on desk 1 fillslots_url = '/api/agenda/%s/fillslots/' % agenda_id resp = app.post(fillslots_url, params={'slots': slots}) assert resp.json['err'] == 0 desk1 = resp.json['desk']['slug'] cancel_url = resp.json['api']['cancel_url'] assert get_free_places() == start_free_places # booking same slots again, will be on desk 2 resp = app.post(fillslots_url, params={'slots': slots}) assert resp.json['err'] == 0 assert resp.json['desk']['slug'] != desk2 # 3 places are disabled in datetimes list assert get_free_places() == start_free_places - len(slots) # try booking again: no desk available resp = app.post(fillslots_url, params={'slots': slots}) assert resp.json['err'] == 1 assert resp.json['reason'] == 'no more desk available' # legacy assert resp.json['err_class'] == 'no more desk available' assert resp.json['err_desc'] == 'no more desk available' assert get_free_places() == start_free_places - len(slots) # cancel desk 1 booking resp = app.post(cancel_url) assert resp.json['err'] == 0 # all places are free again assert get_free_places() == start_free_places # booking a single slot (must be on desk 1) resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, slots[1])) assert resp.json['err'] == 0 assert resp.json['desk']['slug'] == desk1 cancel_url = resp.json['api']['cancel_url'] assert get_free_places() == start_free_places - 1 # try booking the 3 slots again: no desk available, one slot is not fully available resp = app.post(fillslots_url, params={'slots': slots}) assert resp.json['err'] == 1 assert resp.json['reason'] == 'no more desk available' # legacy assert resp.json['err_class'] == 'no more desk available' assert resp.json['err_desc'] == 'no more desk available' # cancel last signel slot booking, desk1 will be free resp = app.post(cancel_url) assert resp.json['err'] == 0 assert get_free_places() == start_free_places # booking again is ok, on desk 1 resp = app.post(fillslots_url, params={'slots': slots}) assert resp.json['err'] == 0 assert resp.json['desk']['slug'] == desk1 assert get_free_places() == start_free_places - len(slots) def test_agenda_meeting_same_day(app, meetings_agenda, mock_now, user): app.authorization = ('Basic', ('john.doe', 'password')) agenda = Agenda(label='Foo', kind='meetings') agenda.minimal_booking_delay = 0 agenda.maximal_booking_delay = 15 agenda.save() meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30) datetime_url = '/api/agenda/meetings/%s/datetimes/' % meeting_type.id desk1 = Desk.objects.create(label='foo', agenda=agenda) desk2 = Desk.objects.create(label='bar', agenda=agenda) for weekday in range(7): TimePeriod.objects.create( weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk1 ) TimePeriod.objects.create( weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk2 ) resp = app.get(datetime_url) event_data = resp.json['data'][0] # check first proposed date is on the same day unless we're past the last # open timeperiod. event_datetime = datetime.datetime.strptime(event_data['datetime'], '%Y-%m-%d %H:%M:%S').timetuple() assert ( event_datetime[:3] == mock_now.timetuple()[:3] and event_datetime[3:5] >= mock_now.timetuple()[3:5] ) or (event_datetime[:3] > mock_now.timetuple()[:3] and event_datetime[3:5] < mock_now.timetuple()[3:5]) # check booking works first_booking_url = resp.json['data'][0]['api']['fillslot_url'] assert app.post(first_booking_url).json['err'] == 0 assert app.post(first_booking_url).json['err'] == 0 assert app.post(first_booking_url).json['err'] == 1 last_booking_url = resp.json['data'][-1]['api']['fillslot_url'] assert app.post(last_booking_url).json['err'] == 0 assert app.post(last_booking_url).json['err'] == 0 assert app.post(last_booking_url).json['err'] == 1 # check full datetimes are marked as disabled resp = app.get(datetime_url) assert resp.json['data'][0]['disabled'] assert not resp.json['data'][1]['disabled'] assert resp.json['data'][-1]['disabled'] assert not resp.json['data'][-2]['disabled'] def test_agenda_meeting_next_day(app, meetings_agenda, mock_now, user): app.authorization = ('Basic', ('john.doe', 'password')) agenda = Agenda(label='Foo', kind='meetings') agenda.minimal_booking_delay = 1 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 desk = Desk.objects.create(label='foo', agenda=agenda) for weekday in range(7): time_period = TimePeriod.objects.create( weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk ) resp = app.get(datetime_url) event_data = resp.json['data'][0] # check all proposed dates are on the next day tomorrow = mock_now + datetime.timedelta(days=1) event_datetime = datetime.datetime.strptime(event_data['datetime'], '%Y-%m-%d %H:%M:%S').timetuple() assert event_datetime[:3] == tomorrow.timetuple()[:3] # 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'] == 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'] == 1 # check full datetimes are marked as disabled resp = app.get(datetime_url) assert resp.json['data'][0]['disabled'] assert not resp.json['data'][1]['disabled'] assert resp.json['data'][-1]['disabled'] assert not resp.json['data'][-2]['disabled'] def test_agenda_meeting_api_exception(app, meetings_agenda, user): app.authorization = ('Basic', ('john.doe', 'password')) meeting_type = MeetingType.objects.get(agenda=meetings_agenda) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) desk = meetings_agenda.desk_set.first() # test exception at the lowest limit excp1 = TimePeriodException.objects.create( desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), ) resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len(resp2.json['data']) + 4 # test exception at the highest limit excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 22, 11, 0)) excp1.save() resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len(resp2.json['data']) + 2 # add an exception with an end datetime less than excp1 end datetime # and make sure that excp1 end datetime preveil excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 23, 11, 0)) excp1.save() TimePeriodException.objects.create( desk=excp1.desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)), end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), ) resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len(resp2.json['data']) + 6 # with a second desk desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) time_period = desk.timeperiod_set.first() TimePeriod.objects.create( desk=desk2, start_time=time_period.start_time, end_time=time_period.end_time, weekday=time_period.weekday, ) resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed # try to booking just after an exception is set TimePeriodException.objects.create( desk=desk2, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 0)), end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), ) booking_url = resp3.json['data'][0]['api']['fillslot_url'] resp = app.post(booking_url) assert resp.json['err'] == 1 def test_agenda_meeting_api_in_between_exceptions(app, meetings_agenda, user): app.authorization = ('Basic', ('john.doe', 'password')) meeting_type = MeetingType.objects.get(agenda=meetings_agenda) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) desk = meetings_agenda.desk_set.first() # test exception at the lowest limit TimePeriodException.objects.create( desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), ) resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len(resp2.json['data']) + 4 # exclude slots on 2017-05-30 and 2017-07-10 date_2017_05_30 = datetime.datetime(2017, 5, 30).date() date_2017_07_10 = datetime.datetime(2017, 7, 10).date() count_on_2017_05_30 = len( [ datum for datum in resp.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_05_30 ] ) count_on_2017_07_10 = len( [ datum for datum in resp.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_07_10 ] ) TimePeriodException.objects.create( desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 30, 8, 0)), end_datetime=make_aware(datetime.datetime(2017, 5, 30, 18, 0)), ) TimePeriodException.objects.create( desk=desk, start_datetime=make_aware(datetime.datetime(2017, 7, 10, 8, 0)), end_datetime=make_aware(datetime.datetime(2017, 7, 10, 18, 0)), ) resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp2.json['data']) == len(resp3.json['data']) + count_on_2017_05_30 + count_on_2017_07_10 assert ( len( [ datum for datum in resp3.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_05_30 ] ) == 0 ) assert ( len( [ datum for datum in resp3.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_07_10 ] ) == 0 ) # with a second desk with the same time periods desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) for time_period in desk.timeperiod_set.all(): TimePeriod.objects.create( desk=desk2, start_time=time_period.start_time, end_time=time_period.end_time, weekday=time_period.weekday, ) resp4 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) assert len(resp.json['data']) == len(resp4.json['data']) def test_agenda_meeting_api_desk_info(app, meetings_agenda, user): app.authorization = ('Basic', ('john.doe', 'password')) meeting_type = MeetingType.objects.get(agenda=meetings_agenda) desk = meetings_agenda.desk_set.get(slug='desk-1') desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) for time_period in desk.timeperiod_set.all(): TimePeriod.objects.create( desk=desk2, start_time=time_period.start_time, end_time=time_period.end_time, weekday=time_period.weekday, ) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) booking_url = resp.json['data'][0]['api']['fillslot_url'] booking_url2 = resp.json['data'][3]['api']['fillslot_url'] resp = app.post(booking_url) assert resp.json['desk']['label'] == desk.label assert resp.json['desk']['slug'] == desk.slug # book the same slot and make sure desk 2 info are returned resp = app.post(booking_url) assert resp.json['desk']['label'] == desk2.label assert resp.json['desk']['slug'] == desk2.slug # booking slot 3 and make sure desk 1 info are returned resp = app.post(booking_url2) assert resp.json['desk']['label'] == desk.label assert resp.json['desk']['slug'] == desk.slug def test_agenda_meeting_gcd_durations(app, meetings_agenda, user): meetings_agenda.maximal_booking_delay = 8 meetings_agenda.save() time_period = TimePeriod.objects.get(end_time=datetime.time(12, 0)) time_period.end_time = datetime.time(13, 0) time_period.save() meeting_type_30 = MeetingType.objects.get(duration=30) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) assert len(resp.json['data']) == 20 meeting_type_20 = MeetingType(agenda=meetings_agenda, label='Lorem', duration=20) meeting_type_20.save() assert meetings_agenda.get_base_meeting_duration() == 10 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) assert len(resp.json['data']) == 56 # 16:30 is time period end time (17:00) minus meeting type duration assert resp.json['data'][-1]['datetime'] == '2017-05-23 16:30:00' resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) assert len(resp.json['data']) == 58 assert resp.json['data'][-1]['datetime'] == '2017-05-23 16:40:00' resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) event_id = resp.json['data'][0]['id'] app.authorization = ('Basic', ('john.doe', 'password')) app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) assert Booking.objects.count() == 1 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 55 event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id'] resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) assert resp.json['datetime'] == '2017-05-22 10:30:00' assert Booking.objects.count() == 2 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id'] resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) assert resp.json['datetime'] == '2017-05-22 10:50:00' assert Booking.objects.count() == 3 # create a gap resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) event_id = [x for x in resp.json['data'] if not x.get('disabled')][1]['id'] resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) assert resp.json['datetime'] == '2017-05-22 11:30:00' assert Booking.objects.count() == 4 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith( '2017-05-22 12:00:00' ) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith( '2017-05-22 12:00:00' ) def test_agenda_meeting_gcd_durations_and_exceptions(app, meetings_agenda, user): meetings_agenda.maximal_booking_delay = 2 meetings_agenda.save() MeetingType.objects.all().delete() meeting_type_20 = MeetingType(agenda=meetings_agenda, label='Blah 20', duration=20) meeting_type_20.save() meeting_type_40 = MeetingType(agenda=meetings_agenda, label='Blah 40', duration=40) meeting_type_40.save() desk = meetings_agenda.desk_set.all()[0] resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) assert len(resp.json['data']) == 6 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_40.id) assert len(resp.json['data']) == 5 # exception to just leave enough place for a single 20-minutes meeting. TimePeriodException.objects.create( desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 20)), end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), ) resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) assert len(resp.json['data']) == 1 resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_40.id) assert len(resp.json['data']) == 0 def test_datetimes_api_meetings_agenda_start_hour_change(app, meetings_agenda): meeting_type = MeetingType.objects.get(agenda=meetings_agenda) api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug) resp = app.get(api_url) dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') ev = Event( agenda=meetings_agenda, meeting_type=meeting_type, places=1, full=False, start_datetime=make_aware(dt), desk=Desk.objects.first(), ) ev.save() booking = Booking(event=ev) booking.save() resp = app.get(api_url) assert len([x for x in resp.json['data'] if x['disabled']]) == 1 desk = Desk.objects.get(label='Desk 1') # shift opening times by 15 minutes for timeperiod in desk.timeperiod_set.all(): timeperiod.start_time = timeperiod.start_time.replace(minute=15) timeperiod.end_time = timeperiod.end_time.replace(minute=15) timeperiod.save() # two slots should now be marked as disabled as the previous booking spans # them. resp = app.get(api_url) assert len([x for x in resp.json['data'] if x['disabled']]) == 2