0001-start-virtual-agendas-37123.patch
chrono/agendas/migrations/0037_auto_20200210_1620.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-02-10 15:20 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('agendas', '0036_auto_20191223_1758'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AddField( |
|
16 |
model_name='agenda', |
|
17 |
name='real_agendas', |
|
18 |
field=models.ManyToManyField(related_name='virtual_agendas', to='agendas.Agenda'), |
|
19 |
), |
|
20 |
migrations.AlterField( |
|
21 |
model_name='agenda', |
|
22 |
name='kind', |
|
23 |
field=models.CharField( |
|
24 |
choices=[('events', 'Events'), ('meetings', 'Meetings'), ('virtual', 'Virtual')], |
|
25 |
default='events', |
|
26 |
max_length=20, |
|
27 |
verbose_name='Kind', |
|
28 |
), |
|
29 |
), |
|
30 |
] |
chrono/agendas/models.py | ||
---|---|---|
42 | 42 |
AGENDA_KINDS = ( |
43 | 43 |
('events', _('Events')), |
44 | 44 |
('meetings', _('Meetings')), |
45 |
('virtual', _('Virtual')), |
|
45 | 46 |
) |
46 | 47 | |
47 | 48 | |
... | ... | |
76 | 77 |
maximal_booking_delay = models.PositiveIntegerField( |
77 | 78 |
_('Maximal booking delay (in days)'), default=56 |
78 | 79 |
) # eight weeks |
80 |
real_agendas = models.ManyToManyField('self', related_name='virtual_agendas', symmetrical=False) |
|
79 | 81 |
edit_role = models.ForeignKey( |
80 | 82 |
Group, |
81 | 83 |
blank=True, |
... | ... | |
118 | 120 |
group_ids = [x.id for x in user.groups.all()] |
119 | 121 |
return bool(self.view_role_id in group_ids) |
120 | 122 | |
123 |
def accept_meetings(self): |
|
124 |
if self.kind == 'virtual': |
|
125 |
return not self.real_agendas.filter(kind='events').exists() |
|
126 |
return self.kind == 'meetings' |
|
127 | ||
128 |
def get_real_agendas(self): |
|
129 |
if self.kind == 'virtual': |
|
130 |
return self.real_agendas.all() |
|
131 |
return [self] |
|
132 | ||
133 |
def iter_meetingtypes(self): |
|
134 |
""" Expose agenda's meetingtypes. |
|
135 |
straighforward on a real agenda |
|
136 |
On a virtual agenda we expose transient meeting types based on on the |
|
137 |
the real ones shared by every real agendas. |
|
138 |
""" |
|
139 |
if self.kind == 'virtual': |
|
140 |
queryset = ( |
|
141 |
MeetingType.objects.filter(agenda__virtual_agendas__in=[self]) |
|
142 |
.values('slug', 'duration', 'label') |
|
143 |
.annotate(total=Count('*')) |
|
144 |
.filter(total=self.real_agendas.count()) |
|
145 |
) |
|
146 |
return [ |
|
147 |
MeetingType(duration=mt['duration'], label=mt['label'], slug=mt['slug']) |
|
148 |
for mt in queryset.order_by('slug') |
|
149 |
] |
|
150 | ||
151 |
return self.meetingtype_set.all().order_by('slug') |
|
152 | ||
153 |
def get_meetingtype(self, id_=None, slug=None): |
|
154 |
match = id_ or slug |
|
155 |
assert match, 'an identifier or a slug should be specified' |
|
156 | ||
157 |
if self.kind == 'virtual': |
|
158 |
match = id_ or slug |
|
159 |
meeting_type = None |
|
160 |
for mt in self.iter_meetingtypes(): |
|
161 |
if mt.slug == match: |
|
162 |
meeting_type = mt |
|
163 |
break |
|
164 |
if meeting_type is None: |
|
165 |
raise MeetingType.DoesNotExist() |
|
166 |
return meeting_type |
|
167 | ||
168 |
if id_: |
|
169 |
return MeetingType.objects.get(id=id_, agenda=self) |
|
170 |
return MeetingType.objects.get(slug=slug, agenda=self) |
|
171 | ||
121 | 172 |
def get_base_meeting_duration(self): |
122 |
durations = [x.duration for x in MeetingType.objects.filter(agenda=self)]
|
|
173 |
durations = [x.duration for x in self.iter_meetingtypes()]
|
|
123 | 174 |
if not durations: |
124 | 175 |
raise ValueError() |
125 | 176 |
gcd = durations[0] |
... | ... | |
128 | 179 |
return gcd |
129 | 180 | |
130 | 181 |
def export_json(self): |
182 |
# TODO VIRTUAL |
|
131 | 183 |
agenda = { |
132 | 184 |
'label': self.label, |
133 | 185 |
'slug': self.slug, |
... | ... | |
192 | 244 |
self.start_datetime = start_datetime |
193 | 245 |
self.end_datetime = start_datetime + datetime.timedelta(minutes=meeting_type.duration) |
194 | 246 |
self.meeting_type = meeting_type |
195 |
self.id = '%s:%s' % (self.meeting_type.id, start_datetime.strftime('%Y-%m-%d-%H%M'))
|
|
247 |
self.id = '%s:%s' % (meeting_type.id or meeting_type.slug, start_datetime.strftime('%Y-%m-%d-%H%M'))
|
|
196 | 248 |
self.desk = desk |
197 | 249 | |
198 | 250 |
def __str__(self): |
... | ... | |
275 | 327 |
unique_together = ['agenda', 'slug'] |
276 | 328 | |
277 | 329 |
def save(self, *args, **kwargs): |
330 |
assert self.agenda.kind != 'virtual', "a meetingtype can't reference a virtual agenda" |
|
278 | 331 |
if not self.slug: |
279 | 332 |
self.slug = generate_slug(self, agenda=self.agenda) |
280 | 333 |
super(MeetingType, self).save(*args, **kwargs) |
... | ... | |
329 | 382 |
return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT') |
330 | 383 | |
331 | 384 |
def save(self, *args, **kwargs): |
385 |
assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda" |
|
332 | 386 |
self.check_full() |
333 | 387 |
return super(Event, self).save(*args, **kwargs) |
334 | 388 | |
... | ... | |
514 | 568 |
unique_together = ['agenda', 'slug'] |
515 | 569 | |
516 | 570 |
def save(self, *args, **kwargs): |
571 |
assert self.agenda.kind != 'virtual', "a desk can't reference a virtual agenda" |
|
517 | 572 |
if not self.slug: |
518 | 573 |
self.slug = generate_slug(self, agenda=self.agenda) |
519 | 574 |
super(Desk, self).save(*args, **kwargs) |
chrono/api/views.py | ||
---|---|---|
63 | 63 |
} |
64 | 64 | |
65 | 65 |
base_date = now().date() |
66 |
open_slots_by_desk = defaultdict(lambda: Intervals()) |
|
67 |
for time_period in TimePeriod.objects.filter(desk__agenda=agenda): |
|
68 |
duration = ( |
|
69 |
datetime.datetime.combine(base_date, time_period.end_time) |
|
70 |
- datetime.datetime.combine(base_date, time_period.start_time) |
|
71 |
).seconds / 60 |
|
72 |
if duration < meeting_type.duration: |
|
73 |
# skip time period that can't even hold a single meeting |
|
74 |
continue |
|
75 |
for slot in time_period.get_time_slots(**time_period_filters): |
|
76 |
slot.full = False |
|
77 |
open_slots_by_desk[time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot) |
|
66 | ||
67 |
agendas = agenda.get_real_agendas() |
|
68 | ||
69 |
open_slots = {} |
|
70 |
for agenda in agendas: |
|
71 |
open_slots[agenda] = defaultdict(lambda: Intervals()) |
|
72 | ||
73 |
for agenda in agendas: |
|
74 |
for time_period in TimePeriod.objects.filter(desk__agenda=agenda): |
|
75 |
duration = ( |
|
76 |
datetime.datetime.combine(base_date, time_period.end_time) |
|
77 |
- datetime.datetime.combine(base_date, time_period.start_time) |
|
78 |
).seconds / 60 |
|
79 |
if duration < meeting_type.duration: |
|
80 |
# skip time period that can't even hold a single meeting |
|
81 |
continue |
|
82 |
for slot in time_period.get_time_slots(**time_period_filters): |
|
83 |
slot.full = False |
|
84 |
open_slots[agenda][time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot) |
|
78 | 85 | |
79 | 86 |
# remove excluded slot |
80 |
excluded_slot_by_desk = get_exceptions_by_desk(agenda) |
|
81 |
for desk, excluded_interval in excluded_slot_by_desk.items(): |
|
82 |
for interval in excluded_interval: |
|
83 |
begin, end = interval |
|
84 |
open_slots_by_desk[desk].remove_overlap(localtime(begin), localtime(end)) |
|
85 | ||
86 |
for event in ( |
|
87 |
agenda.event_set.filter( |
|
88 |
agenda=agenda, |
|
89 |
start_datetime__gte=min_datetime, |
|
90 |
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration), |
|
91 |
) |
|
92 |
.select_related('meeting_type') |
|
93 |
.exclude(booking__cancellation_datetime__isnull=False) |
|
94 |
): |
|
95 |
for slot in open_slots_by_desk[event.desk_id].search_data(event.start_datetime, event.end_datetime): |
|
96 |
slot.full = True |
|
87 |
for agenda in agendas: |
|
88 |
excluded_slot_by_desk = get_exceptions_by_desk(agenda) |
|
89 | ||
90 |
for desk, excluded_interval in excluded_slot_by_desk.items(): |
|
91 |
for interval in excluded_interval: |
|
92 |
begin, end = interval |
|
93 |
open_slots[agenda][desk].remove_overlap(localtime(begin), localtime(end)) |
|
94 | ||
95 |
for agenda in agendas: |
|
96 |
for event in ( |
|
97 |
agenda.event_set.filter( |
|
98 |
agenda=agenda, |
|
99 |
start_datetime__gte=min_datetime, |
|
100 |
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration), |
|
101 |
) |
|
102 |
.select_related('meeting_type') |
|
103 |
.exclude(booking__cancellation_datetime__isnull=False) |
|
104 |
): |
|
105 |
for slot in open_slots[agenda][event.desk_id].search_data( |
|
106 |
event.start_datetime, event.end_datetime |
|
107 |
): |
|
108 |
slot.full = True |
|
97 | 109 | |
98 | 110 |
slots = [] |
99 |
for desk in open_slots_by_desk: |
|
100 |
slots.extend(open_slots_by_desk[desk].iter_data()) |
|
111 |
for agenda in agendas: |
|
112 |
for desk in open_slots[agenda]: |
|
113 |
slots.extend(open_slots[agenda][desk].iter_data()) |
|
101 | 114 |
slots.sort(key=lambda slot: slot.start_datetime) |
102 | 115 |
return slots |
103 | 116 | |
... | ... | |
118 | 131 |
reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug}) |
119 | 132 |
) |
120 | 133 |
} |
121 |
elif agenda.kind == 'meetings':
|
|
134 |
elif agenda.accept_meetings():
|
|
122 | 135 |
agenda_detail['api'] = { |
123 | 136 |
'meetings_url': request.build_absolute_uri( |
124 | 137 |
reverse('api-agenda-meetings', kwargs={'agenda_identifier': agenda.slug}) |
... | ... | |
270 | 283 |
if agenda_identifier is None: |
271 | 284 |
# legacy access by meeting id |
272 | 285 |
meeting_type = MeetingType.objects.get(id=meeting_identifier) |
286 |
agenda = meeting_type.agenda |
|
273 | 287 |
else: |
274 |
meeting_type = MeetingType.objects.get( |
|
275 |
slug=meeting_identifier, agenda__slug=agenda_identifier |
|
276 |
) |
|
277 |
except (ValueError, MeetingType.DoesNotExist): |
|
278 |
raise Http404() |
|
288 |
agenda = Agenda.objects.get(slug=agenda_identifier) |
|
289 |
meeting_type = agenda.get_meetingtype(slug=meeting_identifier) |
|
279 | 290 | |
280 |
agenda = meeting_type.agenda |
|
291 |
except (ValueError, MeetingType.DoesNotExist, Agenda.DoesNotExist): |
|
292 |
raise Http404() |
|
281 | 293 | |
282 | 294 |
now_datetime = now() |
283 | 295 | |
... | ... | |
328 | 340 |
agenda = Agenda.objects.get(slug=agenda_identifier) |
329 | 341 |
except Agenda.DoesNotExist: |
330 | 342 |
raise Http404() |
331 |
if agenda.kind != 'meetings':
|
|
332 |
raise Http404('agenda found, but it was not a meetings agenda')
|
|
343 |
if not agenda.accept_meetings():
|
|
344 |
raise Http404('agenda found, but it does not accept meetings')
|
|
333 | 345 | |
334 | 346 |
meeting_types = [] |
335 |
for meeting_type in agenda.meetingtype_set.all():
|
|
347 |
for meeting_type in agenda.iter_meetingtypes():
|
|
336 | 348 |
meeting_types.append( |
337 | 349 |
{ |
338 | 350 |
'text': meeting_type.label, |
... | ... | |
517 | 529 | |
518 | 530 |
available_desk = None |
519 | 531 | |
520 |
if agenda.kind == 'meetings':
|
|
532 |
if agenda.accept_meetings():
|
|
521 | 533 |
# slots are actually timeslot ids (meeting_type:start_datetime), not events ids. |
522 | 534 |
# split them back to get both parts |
523 | 535 |
meeting_type_id = slots[0].split(':')[0] |
... | ... | |
548 | 560 |
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M'))) |
549 | 561 | |
550 | 562 |
# get all free slots and separate them by desk |
551 |
all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
|
|
563 |
all_slots = get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id))
|
|
552 | 564 |
all_slots = [slot for slot in all_slots if not slot.full] |
553 | 565 |
datetimes_by_desk = defaultdict(set) |
554 | 566 |
for slot in all_slots: |
555 | 567 |
datetimes_by_desk[slot.desk.id].add(slot.start_datetime) |
556 | 568 | |
569 |
# TODO: fill policy for virtual agendas |
|
557 | 570 |
# search first desk where all requested slots are free |
558 | 571 |
for available_desk_id in sorted(datetimes_by_desk.keys()): |
559 | 572 |
if datetimes.issubset(datetimes_by_desk[available_desk_id]): |
... | ... | |
572 | 585 |
datetimes = list(datetimes) |
573 | 586 |
datetimes.sort() |
574 | 587 | |
588 |
# get a real meeting_type for virtual agenda |
|
589 |
if agenda.kind == 'virtual': |
|
590 |
meeting_type_id = MeetingType.objects.get( |
|
591 |
agenda=available_desk.agenda, slug=meeting_type_id |
|
592 |
).pk |
|
593 | ||
575 | 594 |
# booking requires real Event objects (not lazy Timeslots); |
576 | 595 |
# create them now, with data from the slots and the desk we found. |
577 | 596 |
events = [] |
578 | 597 |
for start_datetime in datetimes: |
579 | 598 |
events.append( |
580 | 599 |
Event.objects.create( |
581 |
agenda=agenda, |
|
600 |
agenda=available_desk.agenda,
|
|
582 | 601 |
meeting_type_id=meeting_type_id, |
583 | 602 |
start_datetime=start_datetime, |
584 | 603 |
full=False, |
... | ... | |
658 | 677 |
response['api']['accept_url'] = request.build_absolute_uri( |
659 | 678 |
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id}) |
660 | 679 |
) |
661 |
if agenda.kind == 'meetings':
|
|
680 |
if agenda.accept_meetings():
|
|
662 | 681 |
response['end_datetime'] = format_response_datetime(events[-1].end_datetime) |
663 | 682 |
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60 |
664 | 683 |
if available_desk: |
tests/test_agendas.py | ||
---|---|---|
498 | 498 |
elif event.label == 'bar': |
499 | 499 |
assert event.booked_places_count == 3 |
500 | 500 |
assert event.waiting_list_count == 0 |
501 | ||
502 | ||
503 |
def test_virtual_agenda_init(): |
|
504 |
agenda1 = Agenda.objects.create(label=u'Agenda 1', kind='meetings') |
|
505 |
agenda2 = Agenda.objects.create(label=u'Agenda 2', kind='meetings') |
|
506 |
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual') |
|
507 |
virt_agenda.real_agendas.add(agenda1, agenda2) |
|
508 |
virt_agenda.save() |
|
509 | ||
510 |
assert virt_agenda.real_agendas.count() == 2 |
|
511 |
assert virt_agenda.real_agendas.get(pk=agenda1.pk) |
|
512 |
assert virt_agenda.real_agendas.get(pk=agenda2.pk) |
|
513 | ||
514 |
for agenda in (agenda1, agenda2): |
|
515 |
assert agenda.virtual_agendas.count() == 1 |
|
516 |
assert agenda.virtual_agendas.get() == virt_agenda |
|
517 | ||
518 | ||
519 |
def test_virtual_agenda_base_meeting_duration(): |
|
520 |
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual') |
|
521 | ||
522 |
with pytest.raises(ValueError): |
|
523 |
virt_agenda.get_base_meeting_duration() |
|
524 | ||
525 |
agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings') |
|
526 |
virt_agenda.real_agendas.add(agenda1) |
|
527 |
virt_agenda.save() |
|
528 | ||
529 |
with pytest.raises(ValueError): |
|
530 |
virt_agenda.get_base_meeting_duration() |
|
531 | ||
532 |
meeting_type = MeetingType(agenda=agenda1, label='Foo', duration=30) |
|
533 |
meeting_type.save() |
|
534 |
assert virt_agenda.get_base_meeting_duration() == 30 |
|
535 | ||
536 |
meeting_type = MeetingType(agenda=agenda1, label='Bar', duration=60) |
|
537 |
meeting_type.save() |
|
538 |
assert virt_agenda.get_base_meeting_duration() == 30 |
|
539 | ||
540 |
agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings') |
|
541 |
virt_agenda.real_agendas.add(agenda2) |
|
542 |
virt_agenda.save() |
|
543 | ||
544 |
meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60) |
|
545 |
meeting_type.save() |
|
546 |
assert virt_agenda.get_base_meeting_duration() == 60 |
tests/test_api.py | ||
---|---|---|
38 | 38 |
settings.TIME_ZONE = request.param |
39 | 39 | |
40 | 40 | |
41 |
# 2017-05-20 -> saturday |
|
41 | 42 |
@pytest.fixture( |
42 | 43 |
params=[ |
43 | 44 |
datetime.datetime(2017, 5, 20, 1, 12), |
... | ... | |
109 | 110 |
return agenda |
110 | 111 | |
111 | 112 | |
113 |
@pytest.fixture |
|
114 |
def virtual_meetings_agenda(meetings_agenda): |
|
115 |
agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') |
|
116 |
agenda.real_agendas.add(meetings_agenda) |
|
117 |
return agenda |
|
118 | ||
119 | ||
112 | 120 |
def test_agendas_api(app, some_data, meetings_agenda): |
113 | 121 |
agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] |
114 | 122 |
agenda2 = Agenda.objects.filter(label=u'Foo bar2')[0] |
123 |
virtual_agenda = Agenda.objects.create( |
|
124 |
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=56 |
|
125 |
) |
|
115 | 126 |
resp = app.get('/api/agenda/') |
116 | 127 |
assert resp.json == { |
117 | 128 |
'data': [ |
... | ... | |
152 | 163 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug, |
153 | 164 |
}, |
154 | 165 |
}, |
166 |
{ |
|
167 |
'text': 'Virtual Agenda', |
|
168 |
'id': 'virtual-agenda', |
|
169 |
'slug': 'virtual-agenda', |
|
170 |
'minimal_booking_delay': 1, |
|
171 |
'maximal_booking_delay': 56, |
|
172 |
'kind': 'virtual', |
|
173 |
'api': { |
|
174 |
'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_agenda.slug, |
|
175 |
'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_agenda.slug, |
|
176 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_agenda.slug, |
|
177 |
}, |
|
178 |
}, |
|
155 | 179 |
] |
156 | 180 |
} |
157 | 181 | |
... | ... | |
2138 | 2162 |
api_url = '/api/agenda/%s/' % agenda.slug |
2139 | 2163 |
resp = app.get(api_url) |
2140 | 2164 |
assert type(resp.json['data']) is dict |
2165 | ||
2166 | ||
2167 |
def test_virtual_agenda_detail(app, virtual_meetings_agenda): |
|
2168 |
resp = app.get('/api/agenda/%s/' % virtual_meetings_agenda.slug) |
|
2169 |
assert resp.json == { |
|
2170 |
'data': { |
|
2171 |
'text': 'Virtual Agenda', |
|
2172 |
'id': 'virtual-agenda', |
|
2173 |
'slug': 'virtual-agenda', |
|
2174 |
'minimal_booking_delay': 1, |
|
2175 |
'maximal_booking_delay': 56, |
|
2176 |
'kind': 'virtual', |
|
2177 |
'api': { |
|
2178 |
'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_meetings_agenda.slug, |
|
2179 |
'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_meetings_agenda.slug, |
|
2180 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_meetings_agenda.slug, |
|
2181 |
}, |
|
2182 |
}, |
|
2183 |
} |
|
2184 | ||
2185 | ||
2186 |
def test_virtual_agendas_meetingtypes_api(app): |
|
2187 |
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual') |
|
2188 | ||
2189 |
# No meetings because no real agenda |
|
2190 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2191 |
assert resp.json == {'data': []} |
|
2192 | ||
2193 |
# One real agenda : every meetings exposed |
|
2194 |
foo_agenda = Agenda.objects.create(label=u'Foo', kind='meetings') |
|
2195 |
MeetingType.objects.create(agenda=foo_agenda, label='Meeting1', duration=30) |
|
2196 |
MeetingType.objects.create(agenda=foo_agenda, label='Meeting2', duration=15) |
|
2197 |
virt_agenda.real_agendas.add(foo_agenda) |
|
2198 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2199 |
assert resp.json == { |
|
2200 |
'data': [ |
|
2201 |
{ |
|
2202 |
'text': 'Meeting1', |
|
2203 |
'id': 'meeting1', |
|
2204 |
'duration': 30, |
|
2205 |
'api': { |
|
2206 |
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/', |
|
2207 |
}, |
|
2208 |
}, |
|
2209 |
{ |
|
2210 |
'text': 'Meeting2', |
|
2211 |
'id': 'meeting2', |
|
2212 |
'duration': 15, |
|
2213 |
'api': { |
|
2214 |
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting2/datetimes/', |
|
2215 |
}, |
|
2216 |
}, |
|
2217 |
] |
|
2218 |
} |
|
2219 | ||
2220 |
# Several real agendas |
|
2221 | ||
2222 |
bar_agenda = Agenda.objects.create(label=u'Bar', kind='meetings') |
|
2223 |
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Bar', duration=30) |
|
2224 |
virt_agenda.real_agendas.add(bar_agenda) |
|
2225 | ||
2226 |
# Bar agenda has no meeting type: no meetings exposed |
|
2227 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2228 |
assert resp.json == {'data': []} |
|
2229 | ||
2230 |
# Bar agenda has a meetings wih different label, slug, duration: no meetings exposed |
|
2231 |
mt = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type Bar', duration=15) |
|
2232 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2233 |
assert resp.json == {'data': []} |
|
2234 | ||
2235 |
# Bar agenda has a meetings wih same label, but different slug and duration: no meetings exposed |
|
2236 |
mt.label = 'Meeting1' |
|
2237 |
mt.save() |
|
2238 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2239 |
assert resp.json == {'data': []} |
|
2240 | ||
2241 |
# Bar agenda has a meetings wih same label and slug, but different duration: no meetings exposed |
|
2242 |
mt.slug = 'meeting1' |
|
2243 |
mt.save() |
|
2244 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2245 |
assert resp.json == {'data': []} |
|
2246 | ||
2247 |
# Bar agenda has a meetings wih same label, slug and duration: only this meeting exposed |
|
2248 |
mt.duration = 30 |
|
2249 |
mt.save() |
|
2250 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2251 |
assert resp.json == { |
|
2252 |
'data': [ |
|
2253 |
{ |
|
2254 |
'text': 'Meeting1', |
|
2255 |
'id': 'meeting1', |
|
2256 |
'duration': 30, |
|
2257 |
'api': { |
|
2258 |
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/', |
|
2259 |
}, |
|
2260 |
}, |
|
2261 |
] |
|
2262 |
} |
|
2263 | ||
2264 | ||
2265 |
def test_virtual_agendas_meetings_datetimes_api(app, virtual_meetings_agenda): |
|
2266 |
real_agenda = virtual_meetings_agenda.real_agendas.first() |
|
2267 |
meeting_type = real_agenda.meetingtype_set.first() |
|
2268 |
default_desk = real_agenda.desk_set.first() |
|
2269 |
# Unkown meeting |
|
2270 |
app.get('/api/agenda/%s/meetings/xxx/datetimes/' % virtual_meetings_agenda.slug, status=404) |
|
2271 | ||
2272 |
virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] |
|
2273 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_meetings_agenda.slug, virt_meeting_type.slug) |
|
2274 |
resp = app.get(api_url) |
|
2275 |
assert len(resp.json['data']) == 144 |
|
2276 | ||
2277 |
virtual_meetings_agenda.minimal_booking_delay = 7 |
|
2278 |
virtual_meetings_agenda.maximal_booking_delay = 28 |
|
2279 |
virtual_meetings_agenda.save() |
|
2280 |
resp = app.get(api_url) |
|
2281 |
assert len(resp.json['data']) == 54 |
|
2282 | ||
2283 |
virtual_meetings_agenda.minimal_booking_delay = 1 |
|
2284 |
virtual_meetings_agenda.maximal_booking_delay = 56 |
|
2285 |
virtual_meetings_agenda.save() |
|
2286 |
resp = app.get(api_url) |
|
2287 |
assert len(resp.json['data']) == 144 |
|
2288 | ||
2289 |
resp = app.get(api_url) |
|
2290 |
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') |
|
2291 |
ev = Event( |
|
2292 |
agenda=real_agenda, |
|
2293 |
meeting_type=meeting_type, |
|
2294 |
places=1, |
|
2295 |
full=False, |
|
2296 |
start_datetime=make_aware(dt), |
|
2297 |
desk=default_desk, |
|
2298 |
) |
|
2299 |
ev.save() |
|
2300 |
booking = Booking(event=ev) |
|
2301 |
booking.save() |
|
2302 |
resp2 = app.get(api_url) |
|
2303 |
assert len(resp2.json['data']) == 144 |
|
2304 |
assert resp.json['data'][0] == resp2.json['data'][0] |
|
2305 |
assert resp.json['data'][1] == resp2.json['data'][1] |
|
2306 |
assert resp.json['data'][2] != resp2.json['data'][2] |
|
2307 |
assert resp.json['data'][2]['disabled'] is False |
|
2308 |
assert resp2.json['data'][2]['disabled'] is True |
|
2309 |
assert resp.json['data'][3] == resp2.json['data'][3] |
|
2310 | ||
2311 |
# test with a timeperiod overlapping current moment, it should get one |
|
2312 |
# datetime for the current timeperiod + two from the next week. |
|
2313 |
if now().time().hour == 23: |
|
2314 |
# skip this part of the test as it would require support for events |
|
2315 |
# crossing midnight |
|
2316 |
return |
|
2317 | ||
2318 |
TimePeriod.objects.filter(desk=default_desk).delete() |
|
2319 |
start_time = localtime(now()) - datetime.timedelta(minutes=10) |
|
2320 |
time_period = TimePeriod( |
|
2321 |
weekday=localtime(now()).weekday(), |
|
2322 |
start_time=start_time, |
|
2323 |
end_time=start_time + datetime.timedelta(hours=1), |
|
2324 |
desk=default_desk, |
|
2325 |
) |
|
2326 |
time_period.save() |
|
2327 |
virtual_meetings_agenda.minimal_booking_delay = 0 |
|
2328 |
virtual_meetings_agenda.maximal_booking_delay = 10 |
|
2329 |
virtual_meetings_agenda.save() |
|
2330 |
resp = app.get(api_url) |
|
2331 |
assert len(resp.json['data']) == 3 |
|
2332 | ||
2333 | ||
2334 |
def test_virtual_agendas_meetings_exception(app, user, virtual_meetings_agenda): |
|
2335 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
2336 |
real_agenda = virtual_meetings_agenda.real_agendas.first() |
|
2337 |
desk = real_agenda.desk_set.first() |
|
2338 |
virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] |
|
2339 |
datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % ( |
|
2340 |
virtual_meetings_agenda.slug, |
|
2341 |
virt_meeting_type.slug, |
|
2342 |
) |
|
2343 |
resp = app.get(datetimes_url) |
|
2344 | ||
2345 |
# test exception at the lowest limit |
|
2346 |
excp1 = TimePeriodException.objects.create( |
|
2347 |
desk=desk, |
|
2348 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), |
|
2349 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), |
|
2350 |
) |
|
2351 |
resp2 = app.get(datetimes_url) |
|
2352 |
assert len(resp.json['data']) == len(resp2.json['data']) + 4 |
|
2353 | ||
2354 |
# test exception at the highest limit |
|
2355 |
excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 22, 11, 0)) |
|
2356 |
excp1.save() |
|
2357 |
resp2 = app.get(datetimes_url) |
|
2358 |
assert len(resp.json['data']) == len(resp2.json['data']) + 2 |
|
2359 | ||
2360 |
# add an exception with an end datetime less than excp1 end datetime |
|
2361 |
# and make sure that excp1 end datetime preveil |
|
2362 |
excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 23, 11, 0)) |
|
2363 |
excp1.save() |
|
2364 | ||
2365 |
TimePeriodException.objects.create( |
|
2366 |
desk=excp1.desk, |
|
2367 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)), |
|
2368 |
end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), |
|
2369 |
) |
|
2370 | ||
2371 |
resp2 = app.get(datetimes_url) |
|
2372 |
assert len(resp.json['data']) == len(resp2.json['data']) + 6 |
|
2373 | ||
2374 |
# with a second desk |
|
2375 |
desk2 = Desk.objects.create(label='Desk 2', agenda=real_agenda) |
|
2376 |
time_period = desk.timeperiod_set.first() |
|
2377 |
TimePeriod.objects.create( |
|
2378 |
desk=desk2, |
|
2379 |
start_time=time_period.start_time, |
|
2380 |
end_time=time_period.end_time, |
|
2381 |
weekday=time_period.weekday, |
|
2382 |
) |
|
2383 |
resp3 = app.get(datetimes_url) |
|
2384 |
assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed |
|
2385 | ||
2386 | ||
2387 |
def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, time_zone, mock_now): |
|
2388 |
foo_agenda = Agenda.objects.create( |
|
2389 |
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2390 |
) |
|
2391 |
foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) |
|
2392 |
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') |
|
2393 | ||
2394 |
test_1st_weekday = (localtime(now()).weekday() + 2) % 7 |
|
2395 |
test_2nd_weekday = (localtime(now()).weekday() + 3) % 7 |
|
2396 |
test_3rd_weekday = (localtime(now()).weekday() + 4) % 7 |
|
2397 |
test_4th_weekday = (localtime(now()).weekday() + 5) % 7 |
|
2398 | ||
2399 |
def create_time_perdiods(desk, end=12): |
|
2400 |
TimePeriod.objects.create( |
|
2401 |
weekday=test_1st_weekday, |
|
2402 |
start_time=datetime.time(10, 0), |
|
2403 |
end_time=datetime.time(end, 0), |
|
2404 |
desk=desk, |
|
2405 |
) |
|
2406 |
TimePeriod.objects.create( |
|
2407 |
weekday=test_2nd_weekday, |
|
2408 |
start_time=datetime.time(10, 0), |
|
2409 |
end_time=datetime.time(end, 0), |
|
2410 |
desk=desk, |
|
2411 |
) |
|
2412 | ||
2413 |
create_time_perdiods(foo_desk_1) |
|
2414 |
virt_agenda = Agenda.objects.create( |
|
2415 |
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2416 |
) |
|
2417 |
virt_agenda.real_agendas.add(foo_agenda) |
|
2418 |
virt_meeting_type = virt_agenda.iter_meetingtypes()[0] |
|
2419 | ||
2420 |
# We are saturday and we can book for next monday and tuesday, 4 slots available each day |
|
2421 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) |
|
2422 |
resp = app.get(api_url) |
|
2423 |
assert len(resp.json['data']) == 8 |
|
2424 |
assert resp.json['data'][0]['id'] == 'meeting-type:2017-05-22-1000' |
|
2425 | ||
2426 |
virt_agenda.maximal_booking_delay = 9 # another monday comes in |
|
2427 |
virt_agenda.save() |
|
2428 |
resp = app.get(api_url) |
|
2429 |
assert len(resp.json['data']) == 12 |
|
2430 | ||
2431 |
# Back to next monday and tuesday restriction |
|
2432 |
virt_agenda.maximal_booking_delay = 5 |
|
2433 |
virt_agenda.save() |
|
2434 | ||
2435 |
# Add another agenda |
|
2436 |
bar_agenda = Agenda.objects.create( |
|
2437 |
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2438 |
) |
|
2439 |
bar_meeting_type = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) |
|
2440 |
bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') |
|
2441 |
create_time_perdiods(bar_desk_1, end=13) # bar_agenda has two more slots each day |
|
2442 |
virt_agenda.real_agendas.add(bar_agenda) |
|
2443 |
resp = app.get(api_url) |
|
2444 |
assert len(resp.json['data']) == 12 |
|
2445 | ||
2446 |
# simulate booking |
|
2447 |
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') |
|
2448 |
ev = Event.objects.create( |
|
2449 |
agenda=foo_agenda, |
|
2450 |
meeting_type=foo_meeting_type, |
|
2451 |
places=1, |
|
2452 |
full=False, |
|
2453 |
start_datetime=make_aware(dt), |
|
2454 |
desk=foo_desk_1, |
|
2455 |
) |
|
2456 |
booking1 = Booking.objects.create(event=ev) |
|
2457 | ||
2458 |
resp = app.get(api_url) |
|
2459 |
assert len(resp.json['data']) == 12 |
|
2460 |
# No disabled slot, because the booked slot is still available in second agenda |
|
2461 |
for slot in resp.json['data']: |
|
2462 |
assert slot['disabled'] is False |
|
2463 | ||
2464 |
ev = Event.objects.create( |
|
2465 |
agenda=bar_agenda, |
|
2466 |
meeting_type=bar_meeting_type, |
|
2467 |
places=1, |
|
2468 |
full=False, |
|
2469 |
start_datetime=make_aware(dt), |
|
2470 |
desk=bar_desk_1, |
|
2471 |
) |
|
2472 |
booking2 = Booking.objects.create(event=ev) |
|
2473 | ||
2474 |
resp = app.get(api_url) |
|
2475 |
assert len(resp.json['data']) == 12 |
|
2476 |
# now one slot is disabled |
|
2477 |
for i, slot in enumerate(resp.json['data']): |
|
2478 |
if i == 2: |
|
2479 |
assert slot['disabled'] |
|
2480 |
else: |
|
2481 |
assert slot['disabled'] is False |
|
2482 | ||
2483 |
# Cancel booking, every slot available |
|
2484 |
booking1.cancel() |
|
2485 |
booking2.cancel() |
|
2486 |
resp = app.get(api_url) |
|
2487 |
assert len(resp.json['data']) == 12 |
|
2488 |
for slot in resp.json['data']: |
|
2489 |
assert slot['disabled'] is False |
|
2490 | ||
2491 |
# Add new desk on foo_agenda, open on wednesday |
|
2492 |
foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2') |
|
2493 |
TimePeriod.objects.create( |
|
2494 |
weekday=test_3rd_weekday, |
|
2495 |
start_time=datetime.time(10, 0), |
|
2496 |
end_time=datetime.time(12, 0), |
|
2497 |
desk=foo_desk_2, |
|
2498 |
) |
|
2499 |
resp = app.get(api_url) |
|
2500 |
assert len(resp.json['data']) == 16 |
|
2501 | ||
2502 |
# Add new desk on bar_agenda, open on thursday |
|
2503 |
bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') |
|
2504 |
TimePeriod.objects.create( |
|
2505 |
weekday=test_4th_weekday, |
|
2506 |
start_time=datetime.time(10, 0), |
|
2507 |
end_time=datetime.time(12, 0), |
|
2508 |
desk=bar_desk_2, |
|
2509 |
) |
|
2510 |
resp = app.get(api_url) |
|
2511 |
assert len(resp.json['data']) == 20 |
|
2512 | ||
2513 | ||
2514 |
def test_virtual_agendas_meetings_booking(app, mock_now, user): |
|
2515 |
foo_agenda = Agenda.objects.create( |
|
2516 |
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2517 |
) |
|
2518 |
MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) |
|
2519 |
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') |
|
2520 | ||
2521 |
TimePeriod.objects.create( |
|
2522 |
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1, |
|
2523 |
) |
|
2524 |
TimePeriod.objects.create( |
|
2525 |
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1, |
|
2526 |
) |
|
2527 |
bar_agenda = Agenda.objects.create( |
|
2528 |
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2529 |
) |
|
2530 |
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) |
|
2531 |
bar_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Bar desk 1') |
|
2532 | ||
2533 |
TimePeriod.objects.create( |
|
2534 |
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1, |
|
2535 |
) |
|
2536 |
TimePeriod.objects.create( |
|
2537 |
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1, |
|
2538 |
) |
|
2539 | ||
2540 |
virt_agenda = Agenda.objects.create( |
|
2541 |
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2542 |
) |
|
2543 |
virt_agenda.real_agendas.add(foo_agenda, bar_agenda) |
|
2544 |
virt_meeting_type = virt_agenda.iter_meetingtypes()[0] |
|
2545 |
# We are saturday and we can book for next monday and tuesday, 4 slots available each day |
|
2546 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) |
|
2547 |
resp = app.get(api_url) |
|
2548 |
assert len(resp.json['data']) == 8 |
|
2549 | ||
2550 |
# make a booking |
|
2551 |
fillslot_url = resp.json['data'][0]['api']['fillslot_url'] |
|
2552 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
2553 |
resp_booking = app.post(fillslot_url) |
|
2554 |
assert Booking.objects.count() == 1 |
|
2555 |
booking = Booking.objects.get(pk=resp_booking.json['booking_id']) |
|
2556 |
assert ( |
|
2557 |
resp_booking.json['datetime'] |
|
2558 |
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S') |
|
2559 |
== resp.json['data'][0]['datetime'] |
|
2560 |
) |
|
2561 | ||
2562 |
assert resp_booking.json['end_datetime'] == localtime( |
|
2563 |
Booking.objects.all()[0].event.end_datetime |
|
2564 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
2565 |
assert resp_booking.json['duration'] == 30 |
|
2566 | ||
2567 |
# second booking on the same slot (available on the second real agenda) |
|
2568 |
resp_booking = app.post(fillslot_url) |
|
2569 |
assert Booking.objects.count() == 2 |
|
2570 |
booking = Booking.objects.get(pk=resp_booking.json['booking_id']) |
|
2571 |
assert ( |
|
2572 |
resp_booking.json['datetime'] |
|
2573 |
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S') |
|
2574 |
== resp.json['data'][0]['datetime'] |
|
2575 |
) |
|
2576 | ||
2577 |
# try booking the same timeslot a third time: full |
|
2578 |
resp_booking = app.post(fillslot_url) |
|
2579 |
assert resp_booking.json['err'] == 1 |
|
2580 |
assert resp_booking.json['err_class'] == 'no more desk available' |
|
2581 |
assert resp_booking.json['err_desc'] == 'no more desk available' |
|
2141 |
- |