start virtual agendas (#37123)

This commit is contained in:
Emmanuel Cazenave 2020-02-06 17:49:38 +01:00 committed by Frédéric Péters
parent f675e0e7ef
commit 565d471d07
5 changed files with 691 additions and 47 deletions

View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2020-02-20 12:15
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agendas', '0037_timeperiodexceptionsource_ics_file'),
]
operations = [
migrations.CreateModel(
name='VirtualMember',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
],
),
migrations.AlterField(
model_name='agenda',
name='kind',
field=models.CharField(
choices=[('events', 'Events'), ('meetings', 'Meetings'), ('virtual', 'Virtual')],
default='events',
max_length=20,
verbose_name='Kind',
),
),
migrations.AddField(
model_name='virtualmember',
name='real_agenda',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='virtual_members',
to='agendas.Agenda',
verbose_name='Agenda',
),
),
migrations.AddField(
model_name='virtualmember',
name='virtual_agenda',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name='real_members', to='agendas.Agenda'
),
),
migrations.AddField(
model_name='agenda',
name='real_agendas',
field=models.ManyToManyField(
related_name='virtual_agendas', through='agendas.VirtualMember', to='agendas.Agenda'
),
),
]

View File

@ -43,6 +43,7 @@ from ..interval import Intervals
AGENDA_KINDS = (
('events', _('Events')),
('meetings', _('Meetings')),
('virtual', _('Virtual')),
)
@ -77,6 +78,13 @@ class Agenda(models.Model):
maximal_booking_delay = models.PositiveIntegerField(
_('Maximal booking delay (in days)'), default=56
) # eight weeks
real_agendas = models.ManyToManyField(
'self',
related_name='virtual_agendas',
symmetrical=False,
through='VirtualMember',
through_fields=('virtual_agenda', 'real_agenda'),
)
edit_role = models.ForeignKey(
Group,
blank=True,
@ -119,8 +127,57 @@ class Agenda(models.Model):
group_ids = [x.id for x in user.groups.all()]
return bool(self.view_role_id in group_ids)
def accept_meetings(self):
if self.kind == 'virtual':
return not self.real_agendas.filter(~Q(kind='meetings')).exists()
return self.kind == 'meetings'
def get_real_agendas(self):
if self.kind == 'virtual':
return self.real_agendas.all()
return [self]
def iter_meetingtypes(self):
""" Expose agenda's meetingtypes.
straighforward on a real agenda
On a virtual agenda we expose transient meeting types based on on the
the real ones shared by every real agendas.
"""
if self.kind == 'virtual':
queryset = (
MeetingType.objects.filter(agenda__virtual_agendas__in=[self])
.values('slug', 'duration', 'label')
.annotate(total=Count('*'))
.filter(total=self.real_agendas.count())
)
return [
MeetingType(duration=mt['duration'], label=mt['label'], slug=mt['slug'])
for mt in queryset.order_by('slug')
]
return self.meetingtype_set.all().order_by('slug')
def get_meetingtype(self, id_=None, slug=None):
match = id_ or slug
assert match, 'an identifier or a slug should be specified'
if self.kind == 'virtual':
match = id_ or slug
meeting_type = None
for mt in self.iter_meetingtypes():
if mt.slug == match:
meeting_type = mt
break
if meeting_type is None:
raise MeetingType.DoesNotExist()
return meeting_type
if id_:
return MeetingType.objects.get(id=id_, agenda=self)
return MeetingType.objects.get(slug=slug, agenda=self)
def get_base_meeting_duration(self):
durations = [x.duration for x in MeetingType.objects.filter(agenda=self)]
durations = [x.duration for x in self.iter_meetingtypes()]
if not durations:
raise ValueError()
gcd = durations[0]
@ -129,6 +186,7 @@ class Agenda(models.Model):
return gcd
def export_json(self):
# TODO VIRTUAL
agenda = {
'label': self.label,
'slug': self.slug,
@ -185,6 +243,13 @@ class Agenda(models.Model):
return created
class VirtualMember(models.Model):
virtual_agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE, related_name='real_members')
real_agenda = models.ForeignKey(
Agenda, on_delete=models.CASCADE, related_name='virtual_members', verbose_name='Agenda'
)
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0])
@ -193,7 +258,7 @@ class TimeSlot(object):
self.start_datetime = start_datetime
self.end_datetime = start_datetime + datetime.timedelta(minutes=meeting_type.duration)
self.meeting_type = meeting_type
self.id = '%s:%s' % (self.meeting_type.id, start_datetime.strftime('%Y-%m-%d-%H%M'))
self.id = '%s:%s' % (meeting_type.id or meeting_type.slug, start_datetime.strftime('%Y-%m-%d-%H%M'))
self.desk = desk
def __str__(self):
@ -276,6 +341,7 @@ class MeetingType(models.Model):
unique_together = ['agenda', 'slug']
def save(self, *args, **kwargs):
assert self.agenda.kind != 'virtual', "a meetingtype can't reference a virtual agenda"
if not self.slug:
self.slug = generate_slug(self, agenda=self.agenda)
super(MeetingType, self).save(*args, **kwargs)
@ -330,6 +396,7 @@ class Event(models.Model):
return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT')
def save(self, *args, **kwargs):
assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
self.check_full()
return super(Event, self).save(*args, **kwargs)
@ -536,6 +603,7 @@ class Desk(models.Model):
unique_together = ['agenda', 'slug']
def save(self, *args, **kwargs):
assert self.agenda.kind != 'virtual', "a desk can't reference a virtual agenda"
if not self.slug:
self.slug = generate_slug(self, agenda=self.agenda)
super(Desk, self).save(*args, **kwargs)

View File

@ -63,41 +63,54 @@ def get_all_slots(agenda, meeting_type):
}
base_date = now().date()
open_slots_by_desk = defaultdict(lambda: Intervals())
for time_period in TimePeriod.objects.filter(desk__agenda=agenda):
duration = (
datetime.datetime.combine(base_date, time_period.end_time)
- datetime.datetime.combine(base_date, time_period.start_time)
).seconds / 60
if duration < meeting_type.duration:
# skip time period that can't even hold a single meeting
continue
for slot in time_period.get_time_slots(**time_period_filters):
slot.full = False
open_slots_by_desk[time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot)
agendas = agenda.get_real_agendas()
open_slots = {}
for agenda in agendas:
open_slots[agenda] = defaultdict(lambda: Intervals())
for agenda in agendas:
for time_period in TimePeriod.objects.filter(desk__agenda=agenda):
duration = (
datetime.datetime.combine(base_date, time_period.end_time)
- datetime.datetime.combine(base_date, time_period.start_time)
).seconds / 60
if duration < meeting_type.duration:
# skip time period that can't even hold a single meeting
continue
for slot in time_period.get_time_slots(**time_period_filters):
slot.full = False
open_slots[agenda][time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot)
# remove excluded slot
excluded_slot_by_desk = get_exceptions_by_desk(agenda)
for desk, excluded_interval in excluded_slot_by_desk.items():
for interval in excluded_interval:
begin, end = interval
open_slots_by_desk[desk].remove_overlap(localtime(begin), localtime(end))
for agenda in agendas:
excluded_slot_by_desk = get_exceptions_by_desk(agenda)
for event in (
agenda.event_set.filter(
agenda=agenda,
start_datetime__gte=min_datetime,
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration),
)
.select_related('meeting_type')
.exclude(booking__cancellation_datetime__isnull=False)
):
for slot in open_slots_by_desk[event.desk_id].search_data(event.start_datetime, event.end_datetime):
slot.full = True
for desk, excluded_interval in excluded_slot_by_desk.items():
for interval in excluded_interval:
begin, end = interval
open_slots[agenda][desk].remove_overlap(localtime(begin), localtime(end))
for agenda in agendas:
for event in (
agenda.event_set.filter(
agenda=agenda,
start_datetime__gte=min_datetime,
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration),
)
.select_related('meeting_type')
.exclude(booking__cancellation_datetime__isnull=False)
):
for slot in open_slots[agenda][event.desk_id].search_data(
event.start_datetime, event.end_datetime
):
slot.full = True
slots = []
for desk in open_slots_by_desk:
slots.extend(open_slots_by_desk[desk].iter_data())
for agenda in agendas:
for desk in open_slots[agenda]:
slots.extend(open_slots[agenda][desk].iter_data())
slots.sort(key=lambda slot: slot.start_datetime)
return slots
@ -118,7 +131,7 @@ def get_agenda_detail(request, agenda):
reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug})
)
}
elif agenda.kind == 'meetings':
elif agenda.accept_meetings():
agenda_detail['api'] = {
'meetings_url': request.build_absolute_uri(
reverse('api-agenda-meetings', kwargs={'agenda_identifier': agenda.slug})
@ -269,14 +282,13 @@ class MeetingDatetimes(APIView):
if agenda_identifier is None:
# legacy access by meeting id
meeting_type = MeetingType.objects.get(id=meeting_identifier)
agenda = meeting_type.agenda
else:
meeting_type = MeetingType.objects.get(
slug=meeting_identifier, agenda__slug=agenda_identifier
)
except (ValueError, MeetingType.DoesNotExist):
raise Http404()
agenda = Agenda.objects.get(slug=agenda_identifier)
meeting_type = agenda.get_meetingtype(slug=meeting_identifier)
agenda = meeting_type.agenda
except (ValueError, MeetingType.DoesNotExist, Agenda.DoesNotExist):
raise Http404()
now_datetime = now()
@ -327,11 +339,11 @@ class MeetingList(APIView):
agenda = Agenda.objects.get(slug=agenda_identifier)
except Agenda.DoesNotExist:
raise Http404()
if agenda.kind != 'meetings':
raise Http404('agenda found, but it was not a meetings agenda')
if not agenda.accept_meetings():
raise Http404('agenda found, but it does not accept meetings')
meeting_types = []
for meeting_type in agenda.meetingtype_set.all():
for meeting_type in agenda.iter_meetingtypes():
meeting_types.append(
{
'text': meeting_type.label,
@ -517,7 +529,7 @@ class Fillslots(APIView):
available_desk = None
if agenda.kind == 'meetings':
if agenda.accept_meetings():
# slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
# split them back to get both parts
meeting_type_id = slots[0].split(':')[0]
@ -548,12 +560,13 @@ class Fillslots(APIView):
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
# get all free slots and separate them by desk
all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
all_slots = get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id))
all_slots = [slot for slot in all_slots if not slot.full]
datetimes_by_desk = defaultdict(set)
for slot in all_slots:
datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
# TODO: fill policy for virtual agendas
# search first desk where all requested slots are free
for available_desk_id in sorted(datetimes_by_desk.keys()):
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
@ -572,13 +585,19 @@ class Fillslots(APIView):
datetimes = list(datetimes)
datetimes.sort()
# get a real meeting_type for virtual agenda
if agenda.kind == 'virtual':
meeting_type_id = MeetingType.objects.get(
agenda=available_desk.agenda, slug=meeting_type_id
).pk
# booking requires real Event objects (not lazy Timeslots);
# create them now, with data from the slots and the desk we found.
events = []
for start_datetime in datetimes:
events.append(
Event.objects.create(
agenda=agenda,
agenda=available_desk.agenda,
meeting_type_id=meeting_type_id,
start_datetime=start_datetime,
full=False,
@ -669,7 +688,7 @@ class Fillslots(APIView):
response['api']['suspend_url'] = request.build_absolute_uri(
reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
)
if agenda.kind == 'meetings':
if agenda.accept_meetings():
response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
if available_desk:

View File

@ -18,6 +18,7 @@ from chrono.agendas.models import (
MeetingType,
TimePeriodException,
TimePeriodExceptionSource,
VirtualMember,
)
pytestmark = pytest.mark.django_db
@ -547,3 +548,49 @@ def test_event_bookings_annotation():
elif event.label == 'bar':
assert event.booked_places_count == 3
assert event.waiting_list_count == 0
def test_virtual_agenda_init():
agenda1 = Agenda.objects.create(label=u'Agenda 1', kind='meetings')
agenda2 = Agenda.objects.create(label=u'Agenda 2', kind='meetings')
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
virt_agenda.save()
assert virt_agenda.real_agendas.count() == 2
assert virt_agenda.real_agendas.get(pk=agenda1.pk)
assert virt_agenda.real_agendas.get(pk=agenda2.pk)
for agenda in (agenda1, agenda2):
assert agenda.virtual_agendas.count() == 1
assert agenda.virtual_agendas.get() == virt_agenda
def test_virtual_agenda_base_meeting_duration():
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
with pytest.raises(ValueError):
virt_agenda.get_base_meeting_duration()
agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
with pytest.raises(ValueError):
virt_agenda.get_base_meeting_duration()
meeting_type = MeetingType(agenda=agenda1, label='Foo', duration=30)
meeting_type.save()
assert virt_agenda.get_base_meeting_duration() == 30
meeting_type = MeetingType(agenda=agenda1, label='Bar', duration=60)
meeting_type.save()
assert virt_agenda.get_base_meeting_duration() == 30
agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
virt_agenda.save()
meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60)
meeting_type.save()
assert virt_agenda.get_base_meeting_duration() == 60

View File

@ -11,7 +11,16 @@ 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
from chrono.agendas.models import (
Agenda,
Event,
Booking,
MeetingType,
TimePeriod,
Desk,
TimePeriodException,
VirtualMember,
)
import chrono.api.views
@ -38,6 +47,7 @@ def time_zone(request, settings):
settings.TIME_ZONE = request.param
# 2017-05-20 -> saturday
@pytest.fixture(
params=[
datetime.datetime(2017, 5, 20, 1, 12),
@ -109,9 +119,19 @@ def meetings_agenda(time_zone, mock_now):
return agenda
@pytest.fixture
def virtual_meetings_agenda(meetings_agenda):
agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
VirtualMember.objects.create(virtual_agenda=agenda, real_agenda=meetings_agenda)
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 bar 2')[0]
virtual_agenda = Agenda.objects.create(
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=56
)
resp = app.get('/api/agenda/')
assert resp.json == {
'data': [
@ -152,6 +172,19 @@ def test_agendas_api(app, some_data, meetings_agenda):
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug,
},
},
{
'text': 'Virtual Agenda',
'id': 'virtual-agenda',
'slug': 'virtual-agenda',
'minimal_booking_delay': 1,
'maximal_booking_delay': 56,
'kind': 'virtual',
'api': {
'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_agenda.slug,
'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_agenda.slug,
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_agenda.slug,
},
},
]
}
@ -2423,3 +2456,421 @@ def test_agenda_detail_routing(app, meetings_agenda):
api_url = '/api/agenda/%s/' % agenda.slug
resp = app.get(api_url)
assert type(resp.json['data']) is 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': 1,
'maximal_booking_delay': 56,
'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=u'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=u'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=u'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/',
},
},
]
}
def test_virtual_agendas_meetings_datetimes_api(app, virtual_meetings_agenda):
real_agenda = virtual_meetings_agenda.real_agendas.first()
meeting_type = real_agenda.meetingtype_set.first()
default_desk = real_agenda.desk_set.first()
# Unkown meeting
app.get('/api/agenda/%s/meetings/xxx/datetimes/' % virtual_meetings_agenda.slug, status=404)
virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0]
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_meetings_agenda.slug, virt_meeting_type.slug)
resp = app.get(api_url)
assert len(resp.json['data']) == 144
virtual_meetings_agenda.minimal_booking_delay = 7
virtual_meetings_agenda.maximal_booking_delay = 28
virtual_meetings_agenda.save()
resp = app.get(api_url)
assert len(resp.json['data']) == 54
virtual_meetings_agenda.minimal_booking_delay = 1
virtual_meetings_agenda.maximal_booking_delay = 56
virtual_meetings_agenda.save()
resp = app.get(api_url)
assert len(resp.json['data']) == 144
resp = app.get(api_url)
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
ev = Event(
agenda=real_agenda,
meeting_type=meeting_type,
places=1,
full=False,
start_datetime=make_aware(dt),
desk=default_desk,
)
ev.save()
booking = Booking(event=ev)
booking.save()
resp2 = app.get(api_url)
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
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()
virtual_meetings_agenda.minimal_booking_delay = 0
virtual_meetings_agenda.maximal_booking_delay = 10
virtual_meetings_agenda.save()
resp = app.get(api_url)
assert len(resp.json['data']) == 3
def test_virtual_agendas_meetings_exception(app, user, virtual_meetings_agenda):
app.authorization = ('Basic', ('john.doe', 'password'))
real_agenda = virtual_meetings_agenda.real_agendas.first()
desk = real_agenda.desk_set.first()
virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0]
datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % (
virtual_meetings_agenda.slug,
virt_meeting_type.slug,
)
resp = app.get(datetimes_url)
# 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(datetimes_url)
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(datetimes_url)
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(datetimes_url)
assert len(resp.json['data']) == len(resp2.json['data']) + 6
# with a second desk
desk2 = Desk.objects.create(label='Desk 2', agenda=real_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(datetimes_url)
assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed
def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, time_zone, mock_now):
foo_agenda = Agenda.objects.create(
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
test_1st_weekday = (localtime(now()).weekday() + 2) % 7
test_2nd_weekday = (localtime(now()).weekday() + 3) % 7
test_3rd_weekday = (localtime(now()).weekday() + 4) % 7
test_4th_weekday = (localtime(now()).weekday() + 5) % 7
def create_time_perdiods(desk, end=12):
TimePeriod.objects.create(
weekday=test_1st_weekday,
start_time=datetime.time(10, 0),
end_time=datetime.time(end, 0),
desk=desk,
)
TimePeriod.objects.create(
weekday=test_2nd_weekday,
start_time=datetime.time(10, 0),
end_time=datetime.time(end, 0),
desk=desk,
)
create_time_perdiods(foo_desk_1)
virt_agenda = Agenda.objects.create(
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
# We are saturday and we can book for next monday and tuesday, 4 slots available each day
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
resp = app.get(api_url)
assert len(resp.json['data']) == 8
assert resp.json['data'][0]['id'] == 'meeting-type:2017-05-22-1000'
virt_agenda.maximal_booking_delay = 9 # another monday comes in
virt_agenda.save()
resp = app.get(api_url)
assert len(resp.json['data']) == 12
# Back to next monday and tuesday restriction
virt_agenda.maximal_booking_delay = 5
virt_agenda.save()
# Add another agenda
bar_agenda = Agenda.objects.create(
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
bar_meeting_type = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1')
create_time_perdiods(bar_desk_1, end=13) # bar_agenda has two more slots each day
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
resp = app.get(api_url)
assert len(resp.json['data']) == 12
# simulate booking
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
ev = Event.objects.create(
agenda=foo_agenda,
meeting_type=foo_meeting_type,
places=1,
full=False,
start_datetime=make_aware(dt),
desk=foo_desk_1,
)
booking1 = Booking.objects.create(event=ev)
resp = app.get(api_url)
assert len(resp.json['data']) == 12
# No disabled slot, because the booked slot is still available in second agenda
for slot in resp.json['data']:
assert slot['disabled'] is False
ev = Event.objects.create(
agenda=bar_agenda,
meeting_type=bar_meeting_type,
places=1,
full=False,
start_datetime=make_aware(dt),
desk=bar_desk_1,
)
booking2 = Booking.objects.create(event=ev)
resp = app.get(api_url)
assert len(resp.json['data']) == 12
# now one slot is disabled
for i, slot in enumerate(resp.json['data']):
if i == 2:
assert slot['disabled']
else:
assert slot['disabled'] is False
# Cancel booking, every slot available
booking1.cancel()
booking2.cancel()
resp = app.get(api_url)
assert len(resp.json['data']) == 12
for slot in resp.json['data']:
assert slot['disabled'] is False
# Add new desk on foo_agenda, open on wednesday
foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2')
TimePeriod.objects.create(
weekday=test_3rd_weekday,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=foo_desk_2,
)
resp = app.get(api_url)
assert len(resp.json['data']) == 16
# Add new desk on bar_agenda, open on thursday
bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2')
TimePeriod.objects.create(
weekday=test_4th_weekday,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=bar_desk_2,
)
resp = app.get(api_url)
assert len(resp.json['data']) == 20
def test_virtual_agendas_meetings_booking(app, mock_now, user):
foo_agenda = Agenda.objects.create(
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
TimePeriod.objects.create(
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
)
TimePeriod.objects.create(
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
)
bar_agenda = Agenda.objects.create(
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
)
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
bar_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Bar desk 1')
TimePeriod.objects.create(
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
)
TimePeriod.objects.create(
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
)
virt_agenda = Agenda.objects.create(
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
# We are saturday and we can book for next monday and tuesday, 4 slots available each day
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
resp = app.get(api_url)
assert len(resp.json['data']) == 8
# make a booking
fillslot_url = resp.json['data'][0]['api']['fillslot_url']
app.authorization = ('Basic', ('john.doe', 'password'))
resp_booking = app.post(fillslot_url)
assert Booking.objects.count() == 1
booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
assert (
resp_booking.json['datetime']
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
== resp.json['data'][0]['datetime']
)
assert resp_booking.json['end_datetime'] == localtime(
Booking.objects.all()[0].event.end_datetime
).strftime('%Y-%m-%d %H:%M:%S')
assert resp_booking.json['duration'] == 30
# second booking on the same slot (available on the second real agenda)
resp_booking = app.post(fillslot_url)
assert Booking.objects.count() == 2
booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
assert (
resp_booking.json['datetime']
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
== resp.json['data'][0]['datetime']
)
# try booking the same timeslot a third time: full
resp_booking = app.post(fillslot_url)
assert resp_booking.json['err'] == 1
assert resp_booking.json['err_class'] == 'no more desk available'
assert resp_booking.json['err_desc'] == 'no more desk available'