chrono/tests/api/test_all.py

730 lines
27 KiB
Python

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 (
AbsenceReason,
AbsenceReasonGroup,
Agenda,
Category,
Desk,
Event,
MeetingType,
Resource,
TimePeriodException,
VirtualMember,
)
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')
group = AbsenceReasonGroup.objects.create(label='Foo')
reason = AbsenceReason.objects.create(group=group, label='Foo bar')
reason2 = AbsenceReason.objects.create(group=group, label='Foo bar baz')
event_agenda = Agenda.objects.create(
label='Foo bar', category=category_a, absence_reasons_group=group, 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)
Desk.objects.create(agenda=event_agenda2, slug='_exceptions_holder')
event_agenda3 = Agenda.objects.create(label='Foo bar 3', absence_reasons_group=group)
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
)
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)
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',
'absence_reasons': [
{'id': reason.slug, 'slug': reason.slug, 'text': reason.label, 'label': reason.label},
{'id': reason2.slug, 'slug': reason2.slug, 'text': reason2.label, 'label': reason2.label},
],
'api': {
'datetimes_url': 'http://testserver/api/agenda/foo-bar/datetimes/',
'fillslots_url': 'http://testserver/api/agenda/foo-bar/fillslots/',
},
},
{
'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',
'api': {
'datetimes_url': 'http://testserver/api/agenda/foo-bar-2/datetimes/',
'fillslots_url': 'http://testserver/api/agenda/foo-bar-2/fillslots/',
},
},
{
'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,
'absence_reasons': [
{'id': reason.slug, 'slug': reason.slug, 'text': reason.label, 'label': reason.label},
{'id': reason2.slug, 'slug': reason2.slug, 'text': reason2.label, 'label': reason2.label},
],
'api': {
'datetimes_url': 'http://testserver/api/agenda/foo-bar-3/datetimes/',
'fillslots_url': 'http://testserver/api/agenda/foo-bar-3/fillslots/',
},
},
{
'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',
'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/',
},
},
{
'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,
'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/',
},
},
{
'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,
'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/',
},
},
],
}
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) == 4
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()
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.objects.create(
start_datetime=now(),
places=10,
agenda=event_agenda,
recurrence_days=list(range(7)),
)
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.objects.create(
start_datetime=now(), places=10, agenda=event_agenda, recurrence_days=[now().weekday()]
)
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) == 8
def test_agendas_meetingtypes_api(app, some_data, meetings_agenda):
resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug)
expected_resp = {
'data': [
{
'text': 'Blah',
'id': 'blah',
'duration': 30,
'api': {
'datetimes_url': 'http://testserver/api/agenda/foo-bar-meeting/meetings/blah/datetimes/',
},
}
]
}
assert resp.json == expected_resp
# deleted meeting type does not show up
MeetingType.objects.create(agenda=meetings_agenda, slug='deleted-meeting-type', duration=43, deleted=True)
resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug)
assert resp.json == expected_resp
# wrong kind
agenda1 = Agenda.objects.filter(label='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_meetingtype_api(app):
agenda = Agenda.objects.create(label='Foo bar meeting', kind='meetings')
MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
resp = app.get('/api/agenda/%s/meetings/blah/' % 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/',
},
}
}
# unknown
resp = app.get('/api/agenda/xxxx/meetings/', status=404)
resp = app.get('/api/agenda/%s/meetings/xxxx/' % agenda.slug, status=404)
# wrong kind
agenda.kind = 'virtual'
agenda.save()
resp = app.get('/api/agenda/%s/resources/' % agenda.slug, status=404)
agenda.kind = 'events'
agenda.save()
resp = app.get('/api/agenda/%s/resources/' % agenda.slug, status=404)
def test_agendas_desks_api(app):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
Desk.objects.create(agenda=agenda, label='Desk 1')
resp = app.get('/api/agenda/%s/desks/' % agenda.slug)
assert resp.json == {
'data': [
{
'text': 'Desk 1',
'id': 'desk-1',
}
]
}
# unknown
resp = app.get('/api/agenda/xxxx/resources/', status=404)
# wrong kind
agenda.kind = 'virtual'
agenda.save()
resp = app.get('/api/agenda/%s/resources/' % agenda.slug, status=404)
agenda.kind = 'events'
agenda.save()
resp = app.get('/api/agenda/%s/resources/' % agenda.slug, status=404)
def test_agendas_resources_api(app):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
resource1 = Resource.objects.create(label='Resource 1', description='Foo bar Resource 1')
resource2 = Resource.objects.create(label='Resource 2')
agenda.resources.add(resource1, resource2)
resp = app.get('/api/agenda/%s/resources/' % agenda.slug)
assert resp.json == {
'data': [
{
'text': 'Resource 1',
'id': 'resource-1',
'description': 'Foo bar Resource 1',
},
{
'text': 'Resource 2',
'id': 'resource-2',
'description': '',
},
]
}
# unknown
resp = app.get('/api/agenda/xxxx/resources/', status=404)
# wrong kind
agenda.kind = 'virtual'
agenda.save()
resp = app.get('/api/agenda/%s/resources/' % agenda.slug, status=404)
agenda.kind = 'events'
agenda.save()
resp = app.get('/api/agenda/%s/resources/' % agenda.slug, status=404)
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()
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,
'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,
},
},
}
def test_virtual_agendas_meetingtypes_api(app):
virt_agenda = Agenda.objects.create(label='Virtual agenda', kind='virtual')
# No meetings because no real agenda
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
assert resp.json == {'data': []}
# One real agenda : every meetings exposed
foo_agenda = Agenda.objects.create(label='Foo', kind='meetings')
MeetingType.objects.create(agenda=foo_agenda, label='Meeting1', duration=30)
MeetingType.objects.create(agenda=foo_agenda, label='Meeting2', duration=15)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
assert resp.json == {
'data': [
{
'text': 'Meeting1',
'id': 'meeting1',
'duration': 30,
'api': {
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/',
},
},
{
'text': 'Meeting2',
'id': 'meeting2',
'duration': 15,
'api': {
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting2/datetimes/',
},
},
]
}
# Several real agendas
bar_agenda = Agenda.objects.create(label='Bar', kind='meetings')
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Bar', duration=30)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
# Bar agenda has no meeting type: no meetings exposed
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
assert resp.json == {'data': []}
# Bar agenda has a meetings wih different label, slug, duration: no meetings exposed
mt = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type Bar', duration=15)
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
assert resp.json == {'data': []}
# Bar agenda has a meetings wih same label, but different slug and duration: no meetings exposed
mt.label = 'Meeting1'
mt.save()
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
assert resp.json == {'data': []}
# Bar agenda has a meetings wih same label and slug, but different duration: no meetings exposed
mt.slug = 'meeting1'
mt.save()
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
assert resp.json == {'data': []}
# Bar agenda has a meetings wih same label, slug and duration: only this meeting exposed
mt.duration = 30
mt.save()
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
assert resp.json == {
'data': [
{
'text': 'Meeting1',
'id': 'meeting1',
'duration': 30,
'api': {
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/',
},
},
]
}
@pytest.mark.freeze_time('2021-07-09')
def test_add_agenda(app, user, settings):
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',
}
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'],
}
# 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']
}
# 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
# 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',
}
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