0001-general-exhaustively-list-available-meeting-datetime.patch
chrono/agendas/models.py | ||
---|---|---|
16 | 16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | 17 | |
18 | 18 |
import datetime |
19 |
import fractions |
|
19 | 20 |
import requests |
20 | 21 |
import vobject |
21 | 22 | |
... | ... | |
95 | 96 |
group_ids = [x.id for x in user.groups.all()] |
96 | 97 |
return bool(self.view_role_id in group_ids) |
97 | 98 | |
99 |
def get_base_meeting_duration(self): |
|
100 |
durations = [x.duration for x in MeetingType.objects.filter(agenda=self)] |
|
101 |
if not durations: |
|
102 |
raise ValueError() |
|
103 |
gcd = durations[0] |
|
104 |
for duration in durations[1:]: |
|
105 |
gcd = fractions.gcd(duration, gcd) |
|
106 |
return gcd |
|
107 | ||
98 | 108 |
def export_json(self): |
99 | 109 |
agenda = { |
100 | 110 |
'label': self.label, |
... | ... | |
187 | 197 |
} |
188 | 198 | |
189 | 199 |
def get_time_slots(self, min_datetime, max_datetime, meeting_type): |
190 |
duration = datetime.timedelta(minutes=meeting_type.duration) |
|
200 |
meeting_duration = datetime.timedelta(minutes=meeting_type.duration) |
|
201 |
duration = datetime.timedelta(minutes=self.desk.agenda.get_base_meeting_duration()) |
|
191 | 202 |
min_datetime = make_naive(min_datetime) |
192 | 203 |
max_datetime = make_naive(max_datetime) |
193 | 204 | |
... | ... | |
199 | 210 |
event_datetime = real_min_datetime.replace(hour=self.start_time.hour, |
200 | 211 |
minute=self.start_time.minute, second=0, microsecond=0) |
201 | 212 |
while event_datetime < max_datetime: |
202 |
end_time = event_datetime + duration |
|
213 |
end_time = event_datetime + meeting_duration |
|
214 |
next_time = event_datetime + duration |
|
203 | 215 |
if end_time.time() > self.end_time: |
204 | 216 |
# back to morning |
205 | 217 |
event_datetime = event_datetime.replace(hour=self.start_time.hour, minute=self.start_time.minute) |
206 | 218 |
# but next week |
207 | 219 |
event_datetime += datetime.timedelta(days=7) |
208 |
end_time = event_datetime + duration
|
|
220 |
next_time = event_datetime + duration
|
|
209 | 221 | |
210 | 222 |
if event_datetime > max_datetime: |
211 | 223 |
break |
212 | 224 | |
213 | 225 |
yield TimeSlot(start_datetime=make_aware(event_datetime), meeting_type=meeting_type, desk=self.desk) |
214 |
event_datetime = end_time
|
|
226 |
event_datetime = next_time
|
|
215 | 227 | |
216 | 228 | |
217 | 229 |
class MeetingType(models.Model): |
tests/test_agendas.py | ||
---|---|---|
10 | 10 |
from django.core.management.base import CommandError |
11 | 11 | |
12 | 12 |
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, |
13 |
Desk, TimePeriodException, ICSError) |
|
13 |
Desk, TimePeriod, TimePeriodException, ICSError)
|
|
14 | 14 | |
15 | 15 |
pytestmark = pytest.mark.django_db |
16 | 16 | |
... | ... | |
330 | 330 |
mocked_get.return_value = mocked_response |
331 | 331 |
call_command('sync_desks_timeperiod_exceptions') |
332 | 332 |
assert not TimePeriodException.objects.filter(desk=desk).exists() |
333 | ||
334 |
def test_base_meeting_duration(): |
|
335 |
agenda = Agenda(label='Meeting', kind='meetings') |
|
336 |
agenda.save() |
|
337 | ||
338 |
with pytest.raises(ValueError): |
|
339 |
agenda.get_base_meeting_duration() |
|
340 | ||
341 |
meeting_type = MeetingType(agenda=agenda, label='Foo', duration=30) |
|
342 |
meeting_type.save() |
|
343 |
assert agenda.get_base_meeting_duration() == 30 |
|
344 | ||
345 |
meeting_type = MeetingType(agenda=agenda, label='Bar', duration=60) |
|
346 |
meeting_type.save() |
|
347 |
assert agenda.get_base_meeting_duration() == 30 |
|
348 | ||
349 |
meeting_type = MeetingType(agenda=agenda, label='Bar', duration=45) |
|
350 |
meeting_type.save() |
|
351 |
assert agenda.get_base_meeting_duration() == 15 |
tests/test_api.py | ||
---|---|---|
1023 | 1023 |
resp = app.post(booking_url2) |
1024 | 1024 |
assert resp.json['desk']['label'] == desk.label |
1025 | 1025 |
assert resp.json['desk']['slug'] == desk.slug |
1026 | ||
1027 | ||
1028 |
def test_agenda_meeting_gcd_durations(app, meetings_agenda, user): |
|
1029 |
meetings_agenda.maximal_booking_delay = 8 |
|
1030 |
meetings_agenda.save() |
|
1031 | ||
1032 |
time_period = TimePeriod.objects.get(end_time=datetime.time(12, 0)) |
|
1033 |
time_period.end_time = datetime.time(13, 0) |
|
1034 |
time_period.save() |
|
1035 | ||
1036 |
meeting_type_30 = MeetingType.objects.get(duration=30) |
|
1037 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) |
|
1038 |
assert len(resp.json['data']) == 20 |
|
1039 | ||
1040 |
meeting_type_20 = MeetingType(agenda=meetings_agenda, label='Lorem', duration=20) |
|
1041 |
meeting_type_20.save() |
|
1042 | ||
1043 |
assert meetings_agenda.get_base_meeting_duration() == 10 |
|
1044 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) |
|
1045 |
assert len(resp.json['data']) == 56 |
|
1046 |
# 16:30 is time period end time (17:00) minus meeting type duration |
|
1047 |
assert resp.json['data'][-1]['datetime'] == '2017-05-23 16:30:00' |
|
1048 | ||
1049 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) |
|
1050 |
assert len(resp.json['data']) == 58 |
|
1051 |
assert resp.json['data'][-1]['datetime'] == '2017-05-23 16:40:00' |
|
1052 | ||
1053 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) |
|
1054 |
event_id = resp.json['data'][0]['id'] |
|
1055 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
1056 |
app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) |
|
1057 |
assert Booking.objects.count() == 1 |
|
1058 | ||
1059 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) |
|
1060 |
assert len([x for x in resp.json['data'] if not x.get('disabled')]) == 55 |
|
1061 |
event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id'] |
|
1062 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) |
|
1063 |
assert resp.json['datetime'].startswith('2017-05-22T10:30:00') |
|
1064 |
assert Booking.objects.count() == 2 |
|
1065 | ||
1066 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) |
|
1067 |
event_id = [x for x in resp.json['data'] if not x.get('disabled')][0]['id'] |
|
1068 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) |
|
1069 |
assert resp.json['datetime'].startswith('2017-05-22T10:50:00') |
|
1070 |
assert Booking.objects.count() == 3 |
|
1071 | ||
1072 |
# create a gap |
|
1073 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) |
|
1074 |
event_id = [x for x in resp.json['data'] if not x.get('disabled')][1]['id'] |
|
1075 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (meetings_agenda.id, event_id)) |
|
1076 |
assert resp.json['datetime'].startswith('2017-05-22T11:30:00') |
|
1077 |
assert Booking.objects.count() == 4 |
|
1078 | ||
1079 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) |
|
1080 |
assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith('2017-05-22 12:00:00') |
|
1081 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) |
|
1082 |
assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith('2017-05-22 12:00:00') |
tests/test_time_periods.py | ||
---|---|---|
12 | 12 |
agenda = Agenda(label=u'Foo bar', slug='bar') |
13 | 13 |
agenda.save() |
14 | 14 |
desk = Desk.objects.create(label='Desk 1', agenda=agenda) |
15 |
meeting_type = MeetingType(duration=60, agenda=agenda) |
|
16 |
meeting_type.save() |
|
15 | 17 |
timeperiod = TimePeriod(desk=desk, weekday=0, |
16 | 18 |
start_time=datetime.time(9, 0), |
17 | 19 |
end_time=datetime.time(12, 0)) |
18 | 20 |
events = timeperiod.get_time_slots( |
19 | 21 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
20 | 22 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
21 |
meeting_type=MeetingType(duration=60))
|
|
23 |
meeting_type=meeting_type)
|
|
22 | 24 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
23 | 25 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 5, 9, 0) |
24 | 26 |
assert events[1].start_datetime.timetuple()[:5] == (2016, 9, 5, 10, 0) |
... | ... | |
35 | 37 |
events = timeperiod.get_time_slots( |
36 | 38 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
37 | 39 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
38 |
meeting_type=MeetingType(duration=60))
|
|
40 |
meeting_type=meeting_type)
|
|
39 | 41 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
40 | 42 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 6, 9, 0) |
41 | 43 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 27, 11, 0) |
... | ... | |
48 | 50 |
events = timeperiod.get_time_slots( |
49 | 51 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
50 | 52 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
51 |
meeting_type=MeetingType(duration=60))
|
|
53 |
meeting_type=meeting_type)
|
|
52 | 54 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
53 | 55 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 1, 9, 0) |
54 | 56 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 29, 11, 0) |
... | ... | |
61 | 63 |
events = timeperiod.get_time_slots( |
62 | 64 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
63 | 65 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
64 |
meeting_type=MeetingType(duration=60))
|
|
66 |
meeting_type=meeting_type)
|
|
65 | 67 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
66 | 68 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 2, 9, 0) |
67 | 69 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 30, 11, 0) |
... | ... | |
74 | 76 |
events = timeperiod.get_time_slots( |
75 | 77 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
76 | 78 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
77 |
meeting_type=MeetingType(duration=60))
|
|
79 |
meeting_type=meeting_type)
|
|
78 | 80 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
79 | 81 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0) |
80 | 82 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 0) |
81 | 83 |
assert len(events) == 12 |
82 | 84 | |
83 | 85 |
# shorter duration -> double the events |
86 |
meeting_type.duration = 30 |
|
87 |
meeting_type.save() |
|
84 | 88 |
timeperiod = TimePeriod(desk=desk, weekday=5, |
85 | 89 |
start_time=datetime.time(9, 0), |
86 | 90 |
end_time=datetime.time(12, 0)) |
87 | 91 |
events = timeperiod.get_time_slots( |
88 | 92 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
89 | 93 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
90 |
meeting_type=MeetingType(duration=30))
|
|
94 |
meeting_type=meeting_type)
|
|
91 | 95 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
92 | 96 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0) |
93 | 97 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 30) |
94 |
- |