0001-start-virtual-agendas-37123.patch
chrono/agendas/migrations/0038_start_virtual_agendas.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-02-20 12:15 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 |
import django.db.models.deletion |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
dependencies = [ |
|
12 |
('agendas', '0037_timeperiodexceptionsource_ics_file'), |
|
13 |
] |
|
14 | ||
15 |
operations = [ |
|
16 |
migrations.CreateModel( |
|
17 |
name='VirtualMember', |
|
18 |
fields=[ |
|
19 |
( |
|
20 |
'id', |
|
21 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
22 |
), |
|
23 |
], |
|
24 |
), |
|
25 |
migrations.AlterField( |
|
26 |
model_name='agenda', |
|
27 |
name='kind', |
|
28 |
field=models.CharField( |
|
29 |
choices=[('events', 'Events'), ('meetings', 'Meetings'), ('virtual', 'Virtual')], |
|
30 |
default='events', |
|
31 |
max_length=20, |
|
32 |
verbose_name='Kind', |
|
33 |
), |
|
34 |
), |
|
35 |
migrations.AddField( |
|
36 |
model_name='virtualmember', |
|
37 |
name='real_agenda', |
|
38 |
field=models.ForeignKey( |
|
39 |
on_delete=django.db.models.deletion.CASCADE, |
|
40 |
related_name='virtual_members', |
|
41 |
to='agendas.Agenda', |
|
42 |
verbose_name='Agenda', |
|
43 |
), |
|
44 |
), |
|
45 |
migrations.AddField( |
|
46 |
model_name='virtualmember', |
|
47 |
name='virtual_agenda', |
|
48 |
field=models.ForeignKey( |
|
49 |
on_delete=django.db.models.deletion.CASCADE, related_name='real_members', to='agendas.Agenda' |
|
50 |
), |
|
51 |
), |
|
52 |
migrations.AddField( |
|
53 |
model_name='agenda', |
|
54 |
name='real_agendas', |
|
55 |
field=models.ManyToManyField( |
|
56 |
related_name='virtual_agendas', through='agendas.VirtualMember', to='agendas.Agenda' |
|
57 |
), |
|
58 |
), |
|
59 |
] |
chrono/agendas/models.py | ||
---|---|---|
43 | 43 |
AGENDA_KINDS = ( |
44 | 44 |
('events', _('Events')), |
45 | 45 |
('meetings', _('Meetings')), |
46 |
('virtual', _('Virtual')), |
|
46 | 47 |
) |
47 | 48 | |
48 | 49 | |
... | ... | |
77 | 78 |
maximal_booking_delay = models.PositiveIntegerField( |
78 | 79 |
_('Maximal booking delay (in days)'), default=56 |
79 | 80 |
) # eight weeks |
81 |
real_agendas = models.ManyToManyField( |
|
82 |
'self', |
|
83 |
related_name='virtual_agendas', |
|
84 |
symmetrical=False, |
|
85 |
through='VirtualMember', |
|
86 |
through_fields=('virtual_agenda', 'real_agenda'), |
|
87 |
) |
|
80 | 88 |
edit_role = models.ForeignKey( |
81 | 89 |
Group, |
82 | 90 |
blank=True, |
... | ... | |
119 | 127 |
group_ids = [x.id for x in user.groups.all()] |
120 | 128 |
return bool(self.view_role_id in group_ids) |
121 | 129 | |
130 |
def accept_meetings(self): |
|
131 |
if self.kind == 'virtual': |
|
132 |
return not self.real_agendas.filter(~Q(kind='meetings')).exists() |
|
133 |
return self.kind == 'meetings' |
|
134 | ||
135 |
def get_real_agendas(self): |
|
136 |
if self.kind == 'virtual': |
|
137 |
return self.real_agendas.all() |
|
138 |
return [self] |
|
139 | ||
140 |
def iter_meetingtypes(self): |
|
141 |
""" Expose agenda's meetingtypes. |
|
142 |
straighforward on a real agenda |
|
143 |
On a virtual agenda we expose transient meeting types based on on the |
|
144 |
the real ones shared by every real agendas. |
|
145 |
""" |
|
146 |
if self.kind == 'virtual': |
|
147 |
queryset = ( |
|
148 |
MeetingType.objects.filter(agenda__virtual_agendas__in=[self]) |
|
149 |
.values('slug', 'duration', 'label') |
|
150 |
.annotate(total=Count('*')) |
|
151 |
.filter(total=self.real_agendas.count()) |
|
152 |
) |
|
153 |
return [ |
|
154 |
MeetingType(duration=mt['duration'], label=mt['label'], slug=mt['slug']) |
|
155 |
for mt in queryset.order_by('slug') |
|
156 |
] |
|
157 | ||
158 |
return self.meetingtype_set.all().order_by('slug') |
|
159 | ||
160 |
def get_meetingtype(self, id_=None, slug=None): |
|
161 |
match = id_ or slug |
|
162 |
assert match, 'an identifier or a slug should be specified' |
|
163 | ||
164 |
if self.kind == 'virtual': |
|
165 |
match = id_ or slug |
|
166 |
meeting_type = None |
|
167 |
for mt in self.iter_meetingtypes(): |
|
168 |
if mt.slug == match: |
|
169 |
meeting_type = mt |
|
170 |
break |
|
171 |
if meeting_type is None: |
|
172 |
raise MeetingType.DoesNotExist() |
|
173 |
return meeting_type |
|
174 | ||
175 |
if id_: |
|
176 |
return MeetingType.objects.get(id=id_, agenda=self) |
|
177 |
return MeetingType.objects.get(slug=slug, agenda=self) |
|
178 | ||
122 | 179 |
def get_base_meeting_duration(self): |
123 |
durations = [x.duration for x in MeetingType.objects.filter(agenda=self)]
|
|
180 |
durations = [x.duration for x in self.iter_meetingtypes()]
|
|
124 | 181 |
if not durations: |
125 | 182 |
raise ValueError() |
126 | 183 |
gcd = durations[0] |
... | ... | |
129 | 186 |
return gcd |
130 | 187 | |
131 | 188 |
def export_json(self): |
189 |
# TODO VIRTUAL |
|
132 | 190 |
agenda = { |
133 | 191 |
'label': self.label, |
134 | 192 |
'slug': self.slug, |
... | ... | |
185 | 243 |
return created |
186 | 244 | |
187 | 245 | |
246 |
class VirtualMember(models.Model): |
|
247 |
virtual_agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE, related_name='real_members') |
|
248 |
real_agenda = models.ForeignKey( |
|
249 |
Agenda, on_delete=models.CASCADE, related_name='virtual_members', verbose_name='Agenda' |
|
250 |
) |
|
251 | ||
252 | ||
188 | 253 |
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0]) |
189 | 254 | |
190 | 255 | |
... | ... | |
193 | 258 |
self.start_datetime = start_datetime |
194 | 259 |
self.end_datetime = start_datetime + datetime.timedelta(minutes=meeting_type.duration) |
195 | 260 |
self.meeting_type = meeting_type |
196 |
self.id = '%s:%s' % (self.meeting_type.id, start_datetime.strftime('%Y-%m-%d-%H%M'))
|
|
261 |
self.id = '%s:%s' % (meeting_type.id or meeting_type.slug, start_datetime.strftime('%Y-%m-%d-%H%M'))
|
|
197 | 262 |
self.desk = desk |
198 | 263 | |
199 | 264 |
def __str__(self): |
... | ... | |
276 | 341 |
unique_together = ['agenda', 'slug'] |
277 | 342 | |
278 | 343 |
def save(self, *args, **kwargs): |
344 |
assert self.agenda.kind != 'virtual', "a meetingtype can't reference a virtual agenda" |
|
279 | 345 |
if not self.slug: |
280 | 346 |
self.slug = generate_slug(self, agenda=self.agenda) |
281 | 347 |
super(MeetingType, self).save(*args, **kwargs) |
... | ... | |
330 | 396 |
return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT') |
331 | 397 | |
332 | 398 |
def save(self, *args, **kwargs): |
399 |
assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda" |
|
333 | 400 |
self.check_full() |
334 | 401 |
return super(Event, self).save(*args, **kwargs) |
335 | 402 | |
... | ... | |
521 | 588 |
unique_together = ['agenda', 'slug'] |
522 | 589 | |
523 | 590 |
def save(self, *args, **kwargs): |
591 |
assert self.agenda.kind != 'virtual', "a desk can't reference a virtual agenda" |
|
524 | 592 |
if not self.slug: |
525 | 593 |
self.slug = generate_slug(self, agenda=self.agenda) |
526 | 594 |
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}) |
... | ... | |
269 | 282 |
if agenda_identifier is None: |
270 | 283 |
# legacy access by meeting id |
271 | 284 |
meeting_type = MeetingType.objects.get(id=meeting_identifier) |
285 |
agenda = meeting_type.agenda |
|
272 | 286 |
else: |
273 |
meeting_type = MeetingType.objects.get( |
|
274 |
slug=meeting_identifier, agenda__slug=agenda_identifier |
|
275 |
) |
|
276 |
except (ValueError, MeetingType.DoesNotExist): |
|
277 |
raise Http404() |
|
287 |
agenda = Agenda.objects.get(slug=agenda_identifier) |
|
288 |
meeting_type = agenda.get_meetingtype(slug=meeting_identifier) |
|
278 | 289 | |
279 |
agenda = meeting_type.agenda |
|
290 |
except (ValueError, MeetingType.DoesNotExist, Agenda.DoesNotExist): |
|
291 |
raise Http404() |
|
280 | 292 | |
281 | 293 |
now_datetime = now() |
282 | 294 | |
... | ... | |
327 | 339 |
agenda = Agenda.objects.get(slug=agenda_identifier) |
328 | 340 |
except Agenda.DoesNotExist: |
329 | 341 |
raise Http404() |
330 |
if agenda.kind != 'meetings':
|
|
331 |
raise Http404('agenda found, but it was not a meetings agenda')
|
|
342 |
if not agenda.accept_meetings():
|
|
343 |
raise Http404('agenda found, but it does not accept meetings')
|
|
332 | 344 | |
333 | 345 |
meeting_types = [] |
334 |
for meeting_type in agenda.meetingtype_set.all():
|
|
346 |
for meeting_type in agenda.iter_meetingtypes():
|
|
335 | 347 |
meeting_types.append( |
336 | 348 |
{ |
337 | 349 |
'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, |
... | ... | |
669 | 688 |
response['api']['suspend_url'] = request.build_absolute_uri( |
670 | 689 |
reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk}) |
671 | 690 |
) |
672 |
if agenda.kind == 'meetings':
|
|
691 |
if agenda.accept_meetings():
|
|
673 | 692 |
response['end_datetime'] = format_response_datetime(events[-1].end_datetime) |
674 | 693 |
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60 |
675 | 694 |
if available_desk: |
tests/test_agendas.py | ||
---|---|---|
18 | 18 |
MeetingType, |
19 | 19 |
TimePeriodException, |
20 | 20 |
TimePeriodExceptionSource, |
21 |
VirtualMember, |
|
21 | 22 |
) |
22 | 23 | |
23 | 24 |
pytestmark = pytest.mark.django_db |
... | ... | |
547 | 548 |
elif event.label == 'bar': |
548 | 549 |
assert event.booked_places_count == 3 |
549 | 550 |
assert event.waiting_list_count == 0 |
551 | ||
552 | ||
553 |
def test_virtual_agenda_init(): |
|
554 |
agenda1 = Agenda.objects.create(label=u'Agenda 1', kind='meetings') |
|
555 |
agenda2 = Agenda.objects.create(label=u'Agenda 2', kind='meetings') |
|
556 |
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual') |
|
557 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1) |
|
558 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2) |
|
559 |
virt_agenda.save() |
|
560 | ||
561 |
assert virt_agenda.real_agendas.count() == 2 |
|
562 |
assert virt_agenda.real_agendas.get(pk=agenda1.pk) |
|
563 |
assert virt_agenda.real_agendas.get(pk=agenda2.pk) |
|
564 | ||
565 |
for agenda in (agenda1, agenda2): |
|
566 |
assert agenda.virtual_agendas.count() == 1 |
|
567 |
assert agenda.virtual_agendas.get() == virt_agenda |
|
568 | ||
569 | ||
570 |
def test_virtual_agenda_base_meeting_duration(): |
|
571 |
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual') |
|
572 | ||
573 |
with pytest.raises(ValueError): |
|
574 |
virt_agenda.get_base_meeting_duration() |
|
575 | ||
576 |
agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings') |
|
577 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1) |
|
578 | ||
579 |
with pytest.raises(ValueError): |
|
580 |
virt_agenda.get_base_meeting_duration() |
|
581 | ||
582 |
meeting_type = MeetingType(agenda=agenda1, label='Foo', duration=30) |
|
583 |
meeting_type.save() |
|
584 |
assert virt_agenda.get_base_meeting_duration() == 30 |
|
585 | ||
586 |
meeting_type = MeetingType(agenda=agenda1, label='Bar', duration=60) |
|
587 |
meeting_type.save() |
|
588 |
assert virt_agenda.get_base_meeting_duration() == 30 |
|
589 | ||
590 |
agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings') |
|
591 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2) |
|
592 |
virt_agenda.save() |
|
593 | ||
594 |
meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60) |
|
595 |
meeting_type.save() |
|
596 |
assert virt_agenda.get_base_meeting_duration() == 60 |
tests/test_api.py | ||
---|---|---|
11 | 11 |
from django.test.utils import CaptureQueriesContext |
12 | 12 |
from django.utils.timezone import now, make_aware, localtime |
13 | 13 | |
14 |
from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk, TimePeriodException |
|
14 |
from chrono.agendas.models import ( |
|
15 |
Agenda, |
|
16 |
Event, |
|
17 |
Booking, |
|
18 |
MeetingType, |
|
19 |
TimePeriod, |
|
20 |
Desk, |
|
21 |
TimePeriodException, |
|
22 |
VirtualMember, |
|
23 |
) |
|
15 | 24 |
import chrono.api.views |
16 | 25 | |
17 | 26 | |
... | ... | |
38 | 47 |
settings.TIME_ZONE = request.param |
39 | 48 | |
40 | 49 | |
50 |
# 2017-05-20 -> saturday |
|
41 | 51 |
@pytest.fixture( |
42 | 52 |
params=[ |
43 | 53 |
datetime.datetime(2017, 5, 20, 1, 12), |
... | ... | |
109 | 119 |
return agenda |
110 | 120 | |
111 | 121 | |
122 |
@pytest.fixture |
|
123 |
def virtual_meetings_agenda(meetings_agenda): |
|
124 |
agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual') |
|
125 |
VirtualMember.objects.create(virtual_agenda=agenda, real_agenda=meetings_agenda) |
|
126 |
return agenda |
|
127 | ||
128 | ||
112 | 129 |
def test_agendas_api(app, some_data, meetings_agenda): |
113 | 130 |
agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] |
114 | 131 |
agenda2 = Agenda.objects.filter(label=u'Foo bar 2')[0] |
132 |
virtual_agenda = Agenda.objects.create( |
|
133 |
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=56 |
|
134 |
) |
|
115 | 135 |
resp = app.get('/api/agenda/') |
116 | 136 |
assert resp.json == { |
117 | 137 |
'data': [ |
... | ... | |
152 | 172 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug, |
153 | 173 |
}, |
154 | 174 |
}, |
175 |
{ |
|
176 |
'text': 'Virtual Agenda', |
|
177 |
'id': 'virtual-agenda', |
|
178 |
'slug': 'virtual-agenda', |
|
179 |
'minimal_booking_delay': 1, |
|
180 |
'maximal_booking_delay': 56, |
|
181 |
'kind': 'virtual', |
|
182 |
'api': { |
|
183 |
'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_agenda.slug, |
|
184 |
'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_agenda.slug, |
|
185 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_agenda.slug, |
|
186 |
}, |
|
187 |
}, |
|
155 | 188 |
] |
156 | 189 |
} |
157 | 190 | |
... | ... | |
2252 | 2285 |
api_url = '/api/agenda/%s/' % agenda.slug |
2253 | 2286 |
resp = app.get(api_url) |
2254 | 2287 |
assert type(resp.json['data']) is dict |
2288 | ||
2289 | ||
2290 |
def test_virtual_agenda_detail(app, virtual_meetings_agenda): |
|
2291 |
resp = app.get('/api/agenda/%s/' % virtual_meetings_agenda.slug) |
|
2292 |
assert resp.json == { |
|
2293 |
'data': { |
|
2294 |
'text': 'Virtual Agenda', |
|
2295 |
'id': 'virtual-agenda', |
|
2296 |
'slug': 'virtual-agenda', |
|
2297 |
'minimal_booking_delay': 1, |
|
2298 |
'maximal_booking_delay': 56, |
|
2299 |
'kind': 'virtual', |
|
2300 |
'api': { |
|
2301 |
'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_meetings_agenda.slug, |
|
2302 |
'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_meetings_agenda.slug, |
|
2303 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_meetings_agenda.slug, |
|
2304 |
}, |
|
2305 |
}, |
|
2306 |
} |
|
2307 | ||
2308 | ||
2309 |
def test_virtual_agendas_meetingtypes_api(app): |
|
2310 |
virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual') |
|
2311 | ||
2312 |
# No meetings because no real agenda |
|
2313 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2314 |
assert resp.json == {'data': []} |
|
2315 | ||
2316 |
# One real agenda : every meetings exposed |
|
2317 |
foo_agenda = Agenda.objects.create(label=u'Foo', kind='meetings') |
|
2318 |
MeetingType.objects.create(agenda=foo_agenda, label='Meeting1', duration=30) |
|
2319 |
MeetingType.objects.create(agenda=foo_agenda, label='Meeting2', duration=15) |
|
2320 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) |
|
2321 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2322 |
assert resp.json == { |
|
2323 |
'data': [ |
|
2324 |
{ |
|
2325 |
'text': 'Meeting1', |
|
2326 |
'id': 'meeting1', |
|
2327 |
'duration': 30, |
|
2328 |
'api': { |
|
2329 |
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/', |
|
2330 |
}, |
|
2331 |
}, |
|
2332 |
{ |
|
2333 |
'text': 'Meeting2', |
|
2334 |
'id': 'meeting2', |
|
2335 |
'duration': 15, |
|
2336 |
'api': { |
|
2337 |
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting2/datetimes/', |
|
2338 |
}, |
|
2339 |
}, |
|
2340 |
] |
|
2341 |
} |
|
2342 | ||
2343 |
# Several real agendas |
|
2344 | ||
2345 |
bar_agenda = Agenda.objects.create(label=u'Bar', kind='meetings') |
|
2346 |
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Bar', duration=30) |
|
2347 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) |
|
2348 | ||
2349 |
# Bar agenda has no meeting type: no meetings exposed |
|
2350 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2351 |
assert resp.json == {'data': []} |
|
2352 | ||
2353 |
# Bar agenda has a meetings wih different label, slug, duration: no meetings exposed |
|
2354 |
mt = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type Bar', duration=15) |
|
2355 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2356 |
assert resp.json == {'data': []} |
|
2357 | ||
2358 |
# Bar agenda has a meetings wih same label, but different slug and duration: no meetings exposed |
|
2359 |
mt.label = 'Meeting1' |
|
2360 |
mt.save() |
|
2361 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2362 |
assert resp.json == {'data': []} |
|
2363 | ||
2364 |
# Bar agenda has a meetings wih same label and slug, but different duration: no meetings exposed |
|
2365 |
mt.slug = 'meeting1' |
|
2366 |
mt.save() |
|
2367 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2368 |
assert resp.json == {'data': []} |
|
2369 | ||
2370 |
# Bar agenda has a meetings wih same label, slug and duration: only this meeting exposed |
|
2371 |
mt.duration = 30 |
|
2372 |
mt.save() |
|
2373 |
resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug) |
|
2374 |
assert resp.json == { |
|
2375 |
'data': [ |
|
2376 |
{ |
|
2377 |
'text': 'Meeting1', |
|
2378 |
'id': 'meeting1', |
|
2379 |
'duration': 30, |
|
2380 |
'api': { |
|
2381 |
'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/', |
|
2382 |
}, |
|
2383 |
}, |
|
2384 |
] |
|
2385 |
} |
|
2386 | ||
2387 | ||
2388 |
def test_virtual_agendas_meetings_datetimes_api(app, virtual_meetings_agenda): |
|
2389 |
real_agenda = virtual_meetings_agenda.real_agendas.first() |
|
2390 |
meeting_type = real_agenda.meetingtype_set.first() |
|
2391 |
default_desk = real_agenda.desk_set.first() |
|
2392 |
# Unkown meeting |
|
2393 |
app.get('/api/agenda/%s/meetings/xxx/datetimes/' % virtual_meetings_agenda.slug, status=404) |
|
2394 | ||
2395 |
virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] |
|
2396 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_meetings_agenda.slug, virt_meeting_type.slug) |
|
2397 |
resp = app.get(api_url) |
|
2398 |
assert len(resp.json['data']) == 144 |
|
2399 | ||
2400 |
virtual_meetings_agenda.minimal_booking_delay = 7 |
|
2401 |
virtual_meetings_agenda.maximal_booking_delay = 28 |
|
2402 |
virtual_meetings_agenda.save() |
|
2403 |
resp = app.get(api_url) |
|
2404 |
assert len(resp.json['data']) == 54 |
|
2405 | ||
2406 |
virtual_meetings_agenda.minimal_booking_delay = 1 |
|
2407 |
virtual_meetings_agenda.maximal_booking_delay = 56 |
|
2408 |
virtual_meetings_agenda.save() |
|
2409 |
resp = app.get(api_url) |
|
2410 |
assert len(resp.json['data']) == 144 |
|
2411 | ||
2412 |
resp = app.get(api_url) |
|
2413 |
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') |
|
2414 |
ev = Event( |
|
2415 |
agenda=real_agenda, |
|
2416 |
meeting_type=meeting_type, |
|
2417 |
places=1, |
|
2418 |
full=False, |
|
2419 |
start_datetime=make_aware(dt), |
|
2420 |
desk=default_desk, |
|
2421 |
) |
|
2422 |
ev.save() |
|
2423 |
booking = Booking(event=ev) |
|
2424 |
booking.save() |
|
2425 |
resp2 = app.get(api_url) |
|
2426 |
assert len(resp2.json['data']) == 144 |
|
2427 |
assert resp.json['data'][0] == resp2.json['data'][0] |
|
2428 |
assert resp.json['data'][1] == resp2.json['data'][1] |
|
2429 |
assert resp.json['data'][2] != resp2.json['data'][2] |
|
2430 |
assert resp.json['data'][2]['disabled'] is False |
|
2431 |
assert resp2.json['data'][2]['disabled'] is True |
|
2432 |
assert resp.json['data'][3] == resp2.json['data'][3] |
|
2433 | ||
2434 |
# test with a timeperiod overlapping current moment, it should get one |
|
2435 |
# datetime for the current timeperiod + two from the next week. |
|
2436 |
if now().time().hour == 23: |
|
2437 |
# skip this part of the test as it would require support for events |
|
2438 |
# crossing midnight |
|
2439 |
return |
|
2440 | ||
2441 |
TimePeriod.objects.filter(desk=default_desk).delete() |
|
2442 |
start_time = localtime(now()) - datetime.timedelta(minutes=10) |
|
2443 |
time_period = TimePeriod( |
|
2444 |
weekday=localtime(now()).weekday(), |
|
2445 |
start_time=start_time, |
|
2446 |
end_time=start_time + datetime.timedelta(hours=1), |
|
2447 |
desk=default_desk, |
|
2448 |
) |
|
2449 |
time_period.save() |
|
2450 |
virtual_meetings_agenda.minimal_booking_delay = 0 |
|
2451 |
virtual_meetings_agenda.maximal_booking_delay = 10 |
|
2452 |
virtual_meetings_agenda.save() |
|
2453 |
resp = app.get(api_url) |
|
2454 |
assert len(resp.json['data']) == 3 |
|
2455 | ||
2456 | ||
2457 |
def test_virtual_agendas_meetings_exception(app, user, virtual_meetings_agenda): |
|
2458 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
2459 |
real_agenda = virtual_meetings_agenda.real_agendas.first() |
|
2460 |
desk = real_agenda.desk_set.first() |
|
2461 |
virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0] |
|
2462 |
datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % ( |
|
2463 |
virtual_meetings_agenda.slug, |
|
2464 |
virt_meeting_type.slug, |
|
2465 |
) |
|
2466 |
resp = app.get(datetimes_url) |
|
2467 | ||
2468 |
# test exception at the lowest limit |
|
2469 |
excp1 = TimePeriodException.objects.create( |
|
2470 |
desk=desk, |
|
2471 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), |
|
2472 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), |
|
2473 |
) |
|
2474 |
resp2 = app.get(datetimes_url) |
|
2475 |
assert len(resp.json['data']) == len(resp2.json['data']) + 4 |
|
2476 | ||
2477 |
# test exception at the highest limit |
|
2478 |
excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 22, 11, 0)) |
|
2479 |
excp1.save() |
|
2480 |
resp2 = app.get(datetimes_url) |
|
2481 |
assert len(resp.json['data']) == len(resp2.json['data']) + 2 |
|
2482 | ||
2483 |
# add an exception with an end datetime less than excp1 end datetime |
|
2484 |
# and make sure that excp1 end datetime preveil |
|
2485 |
excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 23, 11, 0)) |
|
2486 |
excp1.save() |
|
2487 | ||
2488 |
TimePeriodException.objects.create( |
|
2489 |
desk=excp1.desk, |
|
2490 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)), |
|
2491 |
end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), |
|
2492 |
) |
|
2493 | ||
2494 |
resp2 = app.get(datetimes_url) |
|
2495 |
assert len(resp.json['data']) == len(resp2.json['data']) + 6 |
|
2496 | ||
2497 |
# with a second desk |
|
2498 |
desk2 = Desk.objects.create(label='Desk 2', agenda=real_agenda) |
|
2499 |
time_period = desk.timeperiod_set.first() |
|
2500 |
TimePeriod.objects.create( |
|
2501 |
desk=desk2, |
|
2502 |
start_time=time_period.start_time, |
|
2503 |
end_time=time_period.end_time, |
|
2504 |
weekday=time_period.weekday, |
|
2505 |
) |
|
2506 |
resp3 = app.get(datetimes_url) |
|
2507 |
assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed |
|
2508 | ||
2509 | ||
2510 |
def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, time_zone, mock_now): |
|
2511 |
foo_agenda = Agenda.objects.create( |
|
2512 |
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2513 |
) |
|
2514 |
foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) |
|
2515 |
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') |
|
2516 | ||
2517 |
test_1st_weekday = (localtime(now()).weekday() + 2) % 7 |
|
2518 |
test_2nd_weekday = (localtime(now()).weekday() + 3) % 7 |
|
2519 |
test_3rd_weekday = (localtime(now()).weekday() + 4) % 7 |
|
2520 |
test_4th_weekday = (localtime(now()).weekday() + 5) % 7 |
|
2521 | ||
2522 |
def create_time_perdiods(desk, end=12): |
|
2523 |
TimePeriod.objects.create( |
|
2524 |
weekday=test_1st_weekday, |
|
2525 |
start_time=datetime.time(10, 0), |
|
2526 |
end_time=datetime.time(end, 0), |
|
2527 |
desk=desk, |
|
2528 |
) |
|
2529 |
TimePeriod.objects.create( |
|
2530 |
weekday=test_2nd_weekday, |
|
2531 |
start_time=datetime.time(10, 0), |
|
2532 |
end_time=datetime.time(end, 0), |
|
2533 |
desk=desk, |
|
2534 |
) |
|
2535 | ||
2536 |
create_time_perdiods(foo_desk_1) |
|
2537 |
virt_agenda = Agenda.objects.create( |
|
2538 |
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2539 |
) |
|
2540 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) |
|
2541 |
virt_meeting_type = virt_agenda.iter_meetingtypes()[0] |
|
2542 | ||
2543 |
# We are saturday and we can book for next monday and tuesday, 4 slots available each day |
|
2544 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) |
|
2545 |
resp = app.get(api_url) |
|
2546 |
assert len(resp.json['data']) == 8 |
|
2547 |
assert resp.json['data'][0]['id'] == 'meeting-type:2017-05-22-1000' |
|
2548 | ||
2549 |
virt_agenda.maximal_booking_delay = 9 # another monday comes in |
|
2550 |
virt_agenda.save() |
|
2551 |
resp = app.get(api_url) |
|
2552 |
assert len(resp.json['data']) == 12 |
|
2553 | ||
2554 |
# Back to next monday and tuesday restriction |
|
2555 |
virt_agenda.maximal_booking_delay = 5 |
|
2556 |
virt_agenda.save() |
|
2557 | ||
2558 |
# Add another agenda |
|
2559 |
bar_agenda = Agenda.objects.create( |
|
2560 |
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2561 |
) |
|
2562 |
bar_meeting_type = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) |
|
2563 |
bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1') |
|
2564 |
create_time_perdiods(bar_desk_1, end=13) # bar_agenda has two more slots each day |
|
2565 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) |
|
2566 |
resp = app.get(api_url) |
|
2567 |
assert len(resp.json['data']) == 12 |
|
2568 | ||
2569 |
# simulate booking |
|
2570 |
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') |
|
2571 |
ev = Event.objects.create( |
|
2572 |
agenda=foo_agenda, |
|
2573 |
meeting_type=foo_meeting_type, |
|
2574 |
places=1, |
|
2575 |
full=False, |
|
2576 |
start_datetime=make_aware(dt), |
|
2577 |
desk=foo_desk_1, |
|
2578 |
) |
|
2579 |
booking1 = Booking.objects.create(event=ev) |
|
2580 | ||
2581 |
resp = app.get(api_url) |
|
2582 |
assert len(resp.json['data']) == 12 |
|
2583 |
# No disabled slot, because the booked slot is still available in second agenda |
|
2584 |
for slot in resp.json['data']: |
|
2585 |
assert slot['disabled'] is False |
|
2586 | ||
2587 |
ev = Event.objects.create( |
|
2588 |
agenda=bar_agenda, |
|
2589 |
meeting_type=bar_meeting_type, |
|
2590 |
places=1, |
|
2591 |
full=False, |
|
2592 |
start_datetime=make_aware(dt), |
|
2593 |
desk=bar_desk_1, |
|
2594 |
) |
|
2595 |
booking2 = Booking.objects.create(event=ev) |
|
2596 | ||
2597 |
resp = app.get(api_url) |
|
2598 |
assert len(resp.json['data']) == 12 |
|
2599 |
# now one slot is disabled |
|
2600 |
for i, slot in enumerate(resp.json['data']): |
|
2601 |
if i == 2: |
|
2602 |
assert slot['disabled'] |
|
2603 |
else: |
|
2604 |
assert slot['disabled'] is False |
|
2605 | ||
2606 |
# Cancel booking, every slot available |
|
2607 |
booking1.cancel() |
|
2608 |
booking2.cancel() |
|
2609 |
resp = app.get(api_url) |
|
2610 |
assert len(resp.json['data']) == 12 |
|
2611 |
for slot in resp.json['data']: |
|
2612 |
assert slot['disabled'] is False |
|
2613 | ||
2614 |
# Add new desk on foo_agenda, open on wednesday |
|
2615 |
foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2') |
|
2616 |
TimePeriod.objects.create( |
|
2617 |
weekday=test_3rd_weekday, |
|
2618 |
start_time=datetime.time(10, 0), |
|
2619 |
end_time=datetime.time(12, 0), |
|
2620 |
desk=foo_desk_2, |
|
2621 |
) |
|
2622 |
resp = app.get(api_url) |
|
2623 |
assert len(resp.json['data']) == 16 |
|
2624 | ||
2625 |
# Add new desk on bar_agenda, open on thursday |
|
2626 |
bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2') |
|
2627 |
TimePeriod.objects.create( |
|
2628 |
weekday=test_4th_weekday, |
|
2629 |
start_time=datetime.time(10, 0), |
|
2630 |
end_time=datetime.time(12, 0), |
|
2631 |
desk=bar_desk_2, |
|
2632 |
) |
|
2633 |
resp = app.get(api_url) |
|
2634 |
assert len(resp.json['data']) == 20 |
|
2635 | ||
2636 | ||
2637 |
def test_virtual_agendas_meetings_booking(app, mock_now, user): |
|
2638 |
foo_agenda = Agenda.objects.create( |
|
2639 |
label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2640 |
) |
|
2641 |
MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30) |
|
2642 |
foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1') |
|
2643 | ||
2644 |
TimePeriod.objects.create( |
|
2645 |
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1, |
|
2646 |
) |
|
2647 |
TimePeriod.objects.create( |
|
2648 |
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1, |
|
2649 |
) |
|
2650 |
bar_agenda = Agenda.objects.create( |
|
2651 |
label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2652 |
) |
|
2653 |
MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30) |
|
2654 |
bar_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Bar desk 1') |
|
2655 | ||
2656 |
TimePeriod.objects.create( |
|
2657 |
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1, |
|
2658 |
) |
|
2659 |
TimePeriod.objects.create( |
|
2660 |
weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1, |
|
2661 |
) |
|
2662 | ||
2663 |
virt_agenda = Agenda.objects.create( |
|
2664 |
label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5 |
|
2665 |
) |
|
2666 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda) |
|
2667 |
VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda) |
|
2668 |
virt_meeting_type = virt_agenda.iter_meetingtypes()[0] |
|
2669 |
# We are saturday and we can book for next monday and tuesday, 4 slots available each day |
|
2670 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug) |
|
2671 |
resp = app.get(api_url) |
|
2672 |
assert len(resp.json['data']) == 8 |
|
2673 | ||
2674 |
# make a booking |
|
2675 |
fillslot_url = resp.json['data'][0]['api']['fillslot_url'] |
|
2676 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
2677 |
resp_booking = app.post(fillslot_url) |
|
2678 |
assert Booking.objects.count() == 1 |
|
2679 |
booking = Booking.objects.get(pk=resp_booking.json['booking_id']) |
|
2680 |
assert ( |
|
2681 |
resp_booking.json['datetime'] |
|
2682 |
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S') |
|
2683 |
== resp.json['data'][0]['datetime'] |
|
2684 |
) |
|
2685 | ||
2686 |
assert resp_booking.json['end_datetime'] == localtime( |
|
2687 |
Booking.objects.all()[0].event.end_datetime |
|
2688 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
2689 |
assert resp_booking.json['duration'] == 30 |
|
2690 | ||
2691 |
# second booking on the same slot (available on the second real agenda) |
|
2692 |
resp_booking = app.post(fillslot_url) |
|
2693 |
assert Booking.objects.count() == 2 |
|
2694 |
booking = Booking.objects.get(pk=resp_booking.json['booking_id']) |
|
2695 |
assert ( |
|
2696 |
resp_booking.json['datetime'] |
|
2697 |
== localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S') |
|
2698 |
== resp.json['data'][0]['datetime'] |
|
2699 |
) |
|
2700 | ||
2701 |
# try booking the same timeslot a third time: full |
|
2702 |
resp_booking = app.post(fillslot_url) |
|
2703 |
assert resp_booking.json['err'] == 1 |
|
2704 |
assert resp_booking.json['err_class'] == 'no more desk available' |
|
2705 |
assert resp_booking.json['err_desc'] == 'no more desk available' |
|
2255 |
- |