import datetime import pytest from django.contrib.auth.models import Group from django.db import connection from django.test.utils import CaptureQueriesContext from django.utils.timezone import localtime, now from chrono.agendas.models import ( Agenda, Booking, Category, Desk, Event, EventsType, Resource, TimePeriodException, ) pytestmark = pytest.mark.django_db def test_agendas_api(app): edit_group = Group.objects.create(name='Edit') view_group = Group.objects.create(name='View') category_a = Category.objects.create(label='Category A') category_b = Category.objects.create(label='Category B') events_type = EventsType.objects.create(label='Type A') events_type2 = EventsType.objects.create(label='Type B') event_agenda = Agenda.objects.create( label='Foo bar', category=category_a, events_type=events_type, edit_role=edit_group, ) Desk.objects.create(agenda=event_agenda, slug='_exceptions_holder') event_agenda2 = Agenda.objects.create(label='Foo bar 2', category=category_a, events_type=events_type2) Desk.objects.create(agenda=event_agenda2, slug='_exceptions_holder') event_agenda3 = Agenda.objects.create(label='Foo bar 3') Desk.objects.create(agenda=event_agenda3, slug='_exceptions_holder') meetings_agenda1 = Agenda.objects.create( label='Foo bar Meeting', kind='meetings', category=category_b, view_role=view_group ) meetings_agenda2 = Agenda.objects.create(label='Foo bar Meeting 2', kind='meetings') resource1 = Resource.objects.create(label='Resource 1', description='Foo bar Resource 1') resource2 = Resource.objects.create(label='Resource 2', description='Foo bar Resource 2') Resource.objects.create(label='Resource 3') meetings_agenda1.resources.add(resource1, resource2) virtual_agenda = Agenda.objects.create( label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=56, edit_role=edit_group, view_role=view_group, ) resp = app.get('/api/agenda/') assert resp.json == { 'err': 0, 'data': [ { 'text': 'Foo bar', 'id': 'foo-bar', 'slug': 'foo-bar', 'kind': 'events', 'minimal_booking_delay': 1, 'minimal_booking_delay_in_working_days': False, 'maximal_booking_delay': 56, 'edit_role': 'Edit', 'view_role': None, 'category': 'category-a', 'category_label': 'Category A', 'events_type': 'type-a', 'api': { 'datetimes_url': 'http://testserver/api/agenda/foo-bar/datetimes/', 'fillslots_url': 'http://testserver/api/agenda/foo-bar/fillslots/', 'backoffice_url': 'http://testserver/manage/agendas/%s/' % event_agenda.pk, }, }, { 'text': 'Foo bar 2', 'id': 'foo-bar-2', 'kind': 'events', 'slug': 'foo-bar-2', 'minimal_booking_delay': 1, 'minimal_booking_delay_in_working_days': False, 'maximal_booking_delay': 56, 'edit_role': None, 'view_role': None, 'category': 'category-a', 'category_label': 'Category A', 'events_type': 'type-b', 'api': { 'datetimes_url': 'http://testserver/api/agenda/foo-bar-2/datetimes/', 'fillslots_url': 'http://testserver/api/agenda/foo-bar-2/fillslots/', 'backoffice_url': 'http://testserver/manage/agendas/%s/' % event_agenda2.pk, }, }, { 'text': 'Foo bar 3', 'id': 'foo-bar-3', 'kind': 'events', 'slug': 'foo-bar-3', 'minimal_booking_delay': 1, 'minimal_booking_delay_in_working_days': False, 'maximal_booking_delay': 56, 'edit_role': None, 'view_role': None, 'category': None, 'category_label': None, 'events_type': None, 'api': { 'datetimes_url': 'http://testserver/api/agenda/foo-bar-3/datetimes/', 'fillslots_url': 'http://testserver/api/agenda/foo-bar-3/fillslots/', 'backoffice_url': 'http://testserver/manage/agendas/%s/' % event_agenda3.pk, }, }, { 'text': 'Foo bar Meeting', 'id': 'foo-bar-meeting', 'slug': 'foo-bar-meeting', 'minimal_booking_delay': 1, 'maximal_booking_delay': 56, 'edit_role': None, 'view_role': 'View', 'category': 'category-b', 'category_label': 'Category B', 'kind': 'meetings', 'resources': [ {'id': 'resource-1', 'text': 'Resource 1', 'description': 'Foo bar Resource 1'}, {'id': 'resource-2', 'text': 'Resource 2', 'description': 'Foo bar Resource 2'}, ], 'api': { 'meetings_url': 'http://testserver/api/agenda/foo-bar-meeting/meetings/', 'desks_url': 'http://testserver/api/agenda/foo-bar-meeting/desks/', 'resources_url': 'http://testserver/api/agenda/foo-bar-meeting/resources/', 'fillslots_url': 'http://testserver/api/agenda/foo-bar-meeting/fillslots/', 'backoffice_url': 'http://testserver/manage/agendas/%s/' % meetings_agenda1.pk, }, }, { 'text': 'Foo bar Meeting 2', 'id': 'foo-bar-meeting-2', 'slug': 'foo-bar-meeting-2', 'minimal_booking_delay': 1, 'maximal_booking_delay': 56, 'edit_role': None, 'view_role': None, 'category': None, 'category_label': None, 'kind': 'meetings', 'resources': [], 'api': { 'meetings_url': 'http://testserver/api/agenda/foo-bar-meeting-2/meetings/', 'desks_url': 'http://testserver/api/agenda/foo-bar-meeting-2/desks/', 'resources_url': 'http://testserver/api/agenda/foo-bar-meeting-2/resources/', 'fillslots_url': 'http://testserver/api/agenda/foo-bar-meeting-2/fillslots/', 'backoffice_url': 'http://testserver/manage/agendas/%s/' % meetings_agenda2.pk, }, }, { 'text': 'Virtual Agenda', 'id': 'virtual-agenda', 'slug': 'virtual-agenda', 'minimal_booking_delay': 1, 'maximal_booking_delay': 56, 'edit_role': 'Edit', 'view_role': 'View', 'category': None, 'category_label': None, 'kind': 'virtual', 'api': { 'meetings_url': 'http://testserver/api/agenda/virtual-agenda/meetings/', 'desks_url': 'http://testserver/api/agenda/virtual-agenda/desks/', 'fillslots_url': 'http://testserver/api/agenda/virtual-agenda/fillslots/', 'backoffice_url': 'http://testserver/manage/agendas/%s/' % virtual_agenda.pk, }, }, ], } resp = app.get('/api/agenda/', params={'q': 'foo'}) assert len(resp.json['data']) == 5 resp = app.get('/api/agenda/', params={'q': 'MEET'}) assert len(resp.json['data']) == 2 resp = app.get('/api/agenda/', params={'q': ''}) assert len(resp.json['data']) == 0 with CaptureQueriesContext(connection) as ctx: resp = app.get('/api/agenda/') assert len(ctx.captured_queries) == 3 with CaptureQueriesContext(connection) as ctx: resp = app.get('/api/agenda/', params={'q': 'MEET'}) assert len(ctx.captured_queries) == 2 resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(resp.json['data']) == 0 resp = app.get('/api/agenda/', params={'category': ''}) assert len(resp.json['data']) == 6 resp = app.get('/api/agenda/', params={'category': '__none__'}) assert len(resp.json['data']) == 3 resp = app.get('/api/agenda/', params={'category': 'category-a'}) assert len(resp.json['data']) == 2 resp = app.get('/api/agenda/', params={'category': 'category-b'}) assert len(resp.json['data']) == 1 resp = app.get('/api/agenda/', params={'category': 'unknown'}) assert len(resp.json['data']) == 0 event1 = Event.objects.create( start_datetime=(localtime() + datetime.timedelta(days=5)).replace(hour=10, minute=0), places=1, agenda=event_agenda, ) event2 = Event.objects.create( start_datetime=(localtime() + datetime.timedelta(days=10)).replace(hour=10, minute=0), places=1, agenda=event_agenda, ) event3 = Event.objects.create( start_datetime=(localtime() + datetime.timedelta(days=15)).replace(hour=10, minute=0), places=1, agenda=event_agenda, ) # all events are free resp = app.get('/api/agenda/', params={'with_open_events': 'true'}) assert len(resp.json['data']) == 1 # one event is full event1.booking_set.create() event1.refresh_from_db() assert event1.full is True resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(resp.json['data']) == 1 # all events are full event2.booking_set.create() event2.refresh_from_db() assert event2.full is True event3.booking_set.create() event3.refresh_from_db() assert event3.full is True resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(resp.json['data']) == 0 # event1 is not full but too soon event1.booking_set.all().delete() event1.refresh_from_db() assert event1.full is False event_agenda.minimal_booking_delay = 10 event_agenda.save() assert list(event_agenda.get_open_events()) == [event2, event3] resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(resp.json['data']) == 0 # event3 is not full but too late event3.booking_set.all().delete() event3.refresh_from_db() assert event3.full is False event_agenda.maximal_booking_delay = 12 event_agenda.save() del event_agenda.max_booking_datetime assert list(event_agenda.get_open_events()) == [event2] resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(resp.json['data']) == 0 # events are not full but not published event2.booking_set.all().delete() event2.refresh_from_db() assert event2.full is False event_agenda.event_set.update(publication_datetime=now() + datetime.timedelta(days=20)) assert list(event_agenda.get_open_events()) == [] resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(resp.json['data']) == 0 # event recurrences are available event = Event.objects.create( start_datetime=now(), places=10, agenda=event_agenda, recurrence_days=list(range(7)), recurrence_end_date=now() + datetime.timedelta(days=15), ) event.create_all_recurrences() assert len(event_agenda.get_open_events()) == 2 resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(resp.json['data']) == 1 for _ in range(10): event_agenda = Agenda.objects.create(label='Foo bar', category=category_a) Desk.objects.create(agenda=event_agenda, slug='_exceptions_holder') event = Event.objects.create( start_datetime=now(), places=10, agenda=event_agenda, recurrence_days=[now().weekday()], recurrence_end_date=now() + datetime.timedelta(days=15), ) event.create_all_recurrences() TimePeriodException.objects.create( desk=event_agenda.desk_set.get(), start_datetime=now(), end_datetime=now() + datetime.timedelta(hours=1), ) with CaptureQueriesContext(connection) as ctx: resp = app.get('/api/agenda/', params={'with_open_events': '1'}) assert len(ctx.captured_queries) == 3 def test_agenda_detail_api(app): agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0) event1 = Event.objects.create( start_datetime=(localtime() + datetime.timedelta(days=5)).replace(hour=10, minute=0), places=1, agenda=agenda, ) event2 = Event.objects.create( start_datetime=(localtime() + datetime.timedelta(days=10)).replace(hour=10, minute=0), places=1, agenda=agenda, ) event3 = Event.objects.create( start_datetime=(localtime() + datetime.timedelta(days=15)).replace(hour=10, minute=0), places=1, agenda=agenda, ) 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['opened_events_available'] is True assert data['api']['datetimes_url'] == 'http://testserver/api/agenda/foo-bar/datetimes/' # one event is full event1.booking_set.create() event1.refresh_from_db() assert event1.full is True resp = app.get('/api/agenda/%s/' % agenda.slug) assert resp.json['data']['opened_events_available'] is True # all events are full event2.booking_set.create() event2.refresh_from_db() assert event2.full is True event3.booking_set.create() event3.refresh_from_db() assert event3.full is True resp = app.get('/api/agenda/%s/' % agenda.slug) assert resp.json['data']['opened_events_available'] is False # event1 is not full but too soon event1.booking_set.all().delete() event1.refresh_from_db() assert event1.full is False agenda.minimal_booking_delay = 10 agenda.save() resp = app.get('/api/agenda/%s/' % agenda.slug) assert list(agenda.get_open_events()) == [event2, event3] assert resp.json['data']['opened_events_available'] is False # event3 is not full but too late event3.booking_set.all().delete() event3.refresh_from_db() assert event3.full is False agenda.maximal_booking_delay = 12 agenda.save() del agenda.max_booking_datetime resp = app.get('/api/agenda/%s/' % agenda.slug) assert list(agenda.get_open_events()) == [event2] assert resp.json['data']['opened_events_available'] is False # events are not full but not published event2.booking_set.all().delete() event2.refresh_from_db() assert event2.full is False agenda.event_set.update(publication_datetime=now() + datetime.timedelta(days=20)) resp = app.get('/api/agenda/%s/' % agenda.slug) assert list(agenda.get_open_events()) == [] assert resp.json['data']['opened_events_available'] is False # unknown app.get('/api/agenda/whatever/', status=404) def test_agenda_detail_routing(app, meetings_agenda): api_url = '/api/agenda/%s/' % meetings_agenda.slug resp = app.get(api_url) assert isinstance(resp.json['data'], dict) # check it doesn't get confused with an agenda with "agenda" in its slug agenda = Agenda( label='Foo bar Agenda', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56 ) agenda.save() api_url = '/api/agenda/%s/' % agenda.slug resp = app.get(api_url) assert isinstance(resp.json['data'], dict) def test_virtual_agenda_detail(app, virtual_meetings_agenda): resp = app.get('/api/agenda/%s/' % virtual_meetings_agenda.slug) assert resp.json == { 'data': { 'text': 'Virtual Agenda', 'id': 'virtual-agenda', 'slug': 'virtual-agenda', 'minimal_booking_delay': None, 'maximal_booking_delay': None, 'edit_role': None, 'view_role': None, 'category': None, 'category_label': None, 'kind': 'virtual', 'api': { 'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_meetings_agenda.slug, 'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_meetings_agenda.slug, 'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_meetings_agenda.slug, 'backoffice_url': 'http://testserver/manage/agendas/%s/' % virtual_meetings_agenda.pk, }, }, } def test_agenda_api_delete(app, user): agenda = Agenda.objects.create(label='Foo bar', kind='events') # unauthenticated resp = app.delete('/api/agenda/%s/' % agenda.slug, status=401) assert Agenda.objects.count() == 1 app.authorization = ('Basic', ('john.doe', 'password')) resp = app.delete('/api/agenda/%s/' % agenda.slug) assert resp.json['err'] == 0 assert not Agenda.objects.exists() def test_agenda_api_delete_busy(app, user): agenda = Agenda.objects.create(label='Foo bar', kind='events') Desk.objects.create(agenda=agenda, slug='_exceptions_holder') event = Event.objects.create(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda) booking = Booking.objects.create(event=event) app.authorization = ('Basic', ('john.doe', 'password')) resp = app.delete('/api/agenda/%s/' % agenda.slug) assert resp.json['err'] == 1 assert 'This cannot be removed' in resp.json['err_desc'] booking.cancellation_datetime = now() booking.save() resp = app.delete('/api/agenda/%s/' % agenda.slug) assert resp.json['err'] == 0 assert not Agenda.objects.exists() @pytest.mark.freeze_time('2021-07-09') def test_add_agenda(app, user, settings): events_type = EventsType.objects.create(label='Type A') category_a = Category.objects.create(label='Category A') api_url = '/api/agenda/' # no authentication resp = app.post(api_url, status=401) assert resp.json['detail'] == 'Authentication credentials were not provided.' # wrong password app.authorization = ('Basic', ('john.doe', 'wrong')) resp = app.post(api_url, status=401) assert resp.json['detail'] == 'Invalid username/password.' app.authorization = ('Basic', ('john.doe', 'password')) # missing fields resp = app.post(api_url, status=400) assert resp.json['err'] assert resp.json['errors'] == {'label': ['This field is required.'], 'slug': ['This field is required.']} # wrong contents params = { 'label': 'foo', 'slug': 'foo', 'kind': 'oups', 'minimal_booking_delay': 'oups', 'minimal_booking_delay_in_working_days': 'oups', 'anonymize_delay': 'oups', 'edit_role': 'oups', 'view_role': 'plop', 'category': 'oups', 'events_type': 'oups', } resp = app.post(api_url, params=params, status=400) assert resp.json['err'] assert resp.json['errors'] == { 'kind': ['"oups" is not a valid choice.'], 'minimal_booking_delay': ['A valid integer is required.'], 'minimal_booking_delay_in_working_days': ['Must be a valid boolean.'], 'anonymize_delay': ['A valid integer is required.'], 'edit_role': ['unknown role: oups'], 'view_role': ['unknown role: plop'], 'category': ['unknown category: oups'], 'events_type': ['unknown events type: oups'], } # slug already used meeting_agenda = Agenda(label='Foo bar Meeting', kind='meetings') meeting_agenda.save() params = { 'label': 'foo', 'slug': meeting_agenda.slug, } resp = app.post(api_url, params=params, status=400) assert resp.json['err'] assert resp.json['errors'] == {'slug': ['agenda with this Identifier already exists.']} # option only available on events agenda params = { 'label': 'foo', 'slug': 'foo', 'kind': 'meetings', 'minimal_booking_delay_in_working_days': True, } resp = app.post(api_url, params=params, status=400) assert resp.json['err'] assert resp.json['errors'] == { 'minimal_booking_delay_in_working_days': ['Option not available on meetings agenda'] } params = { 'label': 'foo', 'slug': 'foo', 'kind': 'meetings', 'events_type': 'type-a', } resp = app.post(api_url, params=params, status=400) assert resp.json['err'] assert resp.json['errors'] == {'events_type': ['Option not available on meetings agenda']} # add an agenda using only required fields params = { 'label': 'My Agenda', 'slug': 'my-agenda', } resp = app.post(api_url, params=params) assert not resp.json['err'] assert len(resp.json['data']) == 1 agenda = Agenda.objects.get(slug='my-agenda') assert agenda.kind == 'events' settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France' edit_group = Group.objects.create(name='Edit') view_group = Group.objects.create(name='View') # add a meetings agenda params = { 'label': 'foo Meetings', 'slug': 'foo-meetings', 'kind': 'meetings', 'minimal_booking_delay': 1, 'maximal_booking_delay': 3, 'anonymize_delay': 30, 'edit_role': 'Edit', 'view_role': 'View', 'category': 'category-a', } resp = app.post(api_url, params=params) assert not resp.json['err'] assert len(resp.json['data']) == 1 agenda = Agenda.objects.get(slug='foo-meetings') assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10) assert agenda.edit_role == edit_group assert agenda.view_role == view_group assert agenda.category == category_a assert not Desk.objects.filter(agenda=agenda, slug='_exceptions_holder').exists() # add an events agenda params = { 'label': 'foo Events', 'slug': 'foo-events', 'kind': 'events', 'minimal_booking_delay': 1, 'minimal_booking_delay_in_working_days': True, 'maximal_booking_delay': 3, 'anonymize_delay': 30, 'edit_role': 'Edit', 'view_role': 'View', 'category': 'category-a', 'events_type': 'type-a', } resp = app.post(api_url, params=params) assert not resp.json['err'] assert len(resp.json['data']) == 1 agenda = Agenda.objects.get(slug='foo-events') assert agenda.edit_role == edit_group assert agenda.view_role == view_group assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 12) assert agenda.category == category_a assert agenda.events_type == events_type assert Desk.objects.filter(agenda=agenda, slug='_exceptions_holder').exists() resp = app.get('/api/agendas/datetimes/?agendas=%s' % agenda.slug) assert 'data' in resp.json