start virtual agendas (#37123)
This commit is contained in:
parent
f675e0e7ef
commit
565d471d07
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue