0004-api-account-for-time-period-weekday-indexes-45159.patch
chrono/agendas/models.py | ||
---|---|---|
53 | 53 |
from django.utils.translation import ungettext |
54 | 54 | |
55 | 55 |
from chrono.interval import Interval, IntervalSet |
56 |
from chrono.utils.date import get_weekday_index |
|
56 | 57 |
from chrono.utils.db import SumCardinality |
57 | 58 |
from chrono.utils.publik_urls import translate_from_publik_url |
58 | 59 |
from chrono.utils.requests_wrapper import requests as requests_wrapper |
... | ... | |
1181 | 1182 |
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0]) |
1182 | 1183 | |
1183 | 1184 | |
1184 |
class WeekTime(collections.namedtuple('WeekTime', ['weekday', 'time'])): |
|
1185 |
class WeekTime(collections.namedtuple('WeekTime', ['weekday', 'weekday_indexes', 'time'])):
|
|
1185 | 1186 |
"""Representation of a time point in a weekday, ex.: Monday at 5 o'clock.""" |
1186 | 1187 | |
1187 | 1188 |
def __repr__(self): |
... | ... | |
1269 | 1270 | |
1270 | 1271 |
def as_weektime_interval(self): |
1271 | 1272 |
return Interval( |
1272 |
WeekTime(self.weekday, self.start_time), |
|
1273 |
WeekTime(self.weekday, self.end_time), |
|
1273 |
WeekTime(self.weekday, self.weekday_indexes, self.start_time),
|
|
1274 |
WeekTime(self.weekday, self.weekday_indexes, self.end_time),
|
|
1274 | 1275 |
) |
1275 | 1276 | |
1276 | 1277 |
def as_shared_timeperiods(self): |
1277 | 1278 |
return SharedTimePeriod( |
1278 | 1279 |
weekday=self.weekday, |
1280 |
weekday_indexes=self.weekday_indexes, |
|
1279 | 1281 |
start_time=self.start_time, |
1280 | 1282 |
end_time=self.end_time, |
1281 | 1283 |
desks=[self.desk], |
... | ... | |
1303 | 1305 |
of get_all_slots() for details). |
1304 | 1306 |
""" |
1305 | 1307 | |
1306 |
__slots__ = ['weekday', 'start_time', 'end_time', 'desks'] |
|
1308 |
__slots__ = ['weekday', 'weekday_indexes', 'start_time', 'end_time', 'desks']
|
|
1307 | 1309 | |
1308 |
def __init__(self, weekday, start_time, end_time, desks): |
|
1310 |
def __init__(self, weekday, weekday_indexes, start_time, end_time, desks):
|
|
1309 | 1311 |
self.weekday = weekday |
1312 |
self.weekday_indexes = weekday_indexes |
|
1310 | 1313 |
self.start_time = start_time |
1311 | 1314 |
self.end_time = end_time |
1312 | 1315 |
self.desks = set(desks) |
... | ... | |
1376 | 1379 |
while event_datetime < max_datetime: |
1377 | 1380 |
end_time = event_datetime + meeting_duration |
1378 | 1381 |
next_time = event_datetime + duration |
1379 |
if end_time.time() > self.end_time or event_datetime.date() != next_time.date(): |
|
1382 |
if ( |
|
1383 |
end_time.time() > self.end_time |
|
1384 |
or event_datetime.date() != next_time.date() |
|
1385 |
or (self.weekday_indexes and get_weekday_index(event_datetime) not in self.weekday_indexes) |
|
1386 |
): |
|
1380 | 1387 |
# switch to naive time for day/week changes |
1381 | 1388 |
event_datetime = make_naive(event_datetime) |
1382 | 1389 |
# back to morning |
... | ... | |
1385 | 1392 |
) |
1386 | 1393 |
# but next week |
1387 | 1394 |
event_datetime += datetime.timedelta(days=7) |
1395 | ||
1388 | 1396 |
# and re-align to timezone afterwards |
1389 | 1397 |
event_datetime = make_aware(event_datetime) |
1390 |
next_time = event_datetime + duration
|
|
1398 |
continue
|
|
1391 | 1399 | |
1392 | 1400 |
# don't end after max_datetime |
1393 | 1401 |
if event_datetime > max_datetime: |
... | ... | |
1399 | 1407 |
@classmethod |
1400 | 1408 |
def from_weektime_interval(cls, weektime_interval, desks=()): |
1401 | 1409 |
begin, end = weektime_interval |
1402 |
assert begin.weekday == end.weekday |
|
1410 |
assert begin.weekday == end.weekday and begin.weekday_indexes == end.weekday_indexes
|
|
1403 | 1411 |
return cls( |
1404 | 1412 |
weekday=begin.weekday, |
1413 |
weekday_indexes=begin.weekday_indexes, |
|
1405 | 1414 |
start_time=begin.time, |
1406 | 1415 |
end_time=end.time, |
1407 | 1416 |
desks=desks, |
tests/api/test_fillslot.py | ||
---|---|---|
889 | 889 |
assert resp.json['data'][event_index]['disabled'] |
890 | 890 | |
891 | 891 | |
892 |
@pytest.mark.freeze_time('2022-01-20 14:00') # Thursday |
|
893 |
def test_booking_api_meeting_weekday_indexes(app, user): |
|
894 |
agenda = Agenda.objects.create( |
|
895 |
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 |
|
896 |
) |
|
897 |
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) |
|
898 |
desk = Desk.objects.create(agenda=agenda, label='desk') |
|
899 | ||
900 |
time_period = TimePeriod.objects.create( |
|
901 |
weekday=3, # Thursday |
|
902 |
weekday_indexes=[1, 3], |
|
903 |
start_time=datetime.time(11, 0), |
|
904 |
end_time=datetime.time(12, 0), |
|
905 |
desk=desk, |
|
906 |
) |
|
907 |
datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug)) |
|
908 |
slot = datetimes_resp.json['data'][0]['id'] |
|
909 |
assert slot == 'plop:2022-02-03-1100' |
|
910 | ||
911 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
912 | ||
913 |
# single booking |
|
914 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot)) |
|
915 |
assert Booking.objects.count() == 1 |
|
916 |
assert resp.json['duration'] == 30 |
|
917 | ||
918 |
# multiple slots |
|
919 |
slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']] |
|
920 |
assert slots == ['plop:2022-02-03-1130', 'plop:2022-02-17-1100'] |
|
921 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots}) |
|
922 |
assert Booking.objects.count() == 3 |
|
923 | ||
924 |
# try to book slot on a skipped week |
|
925 |
slot = datetimes_resp.json['data'][3]['id'] |
|
926 |
time_period.weekday_indexes = [1] |
|
927 |
time_period.save() |
|
928 |
resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug)) |
|
929 |
assert slot not in {slot['id'] for slot in resp.json['data']} |
|
930 | ||
931 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot)) |
|
932 |
assert resp.json['err'] == 1 |
|
933 |
assert resp.json['err_desc'] == 'no more desk available' |
|
934 | ||
935 | ||
892 | 936 |
def test_booking_api_with_data(app, user): |
893 | 937 |
agenda = Agenda.objects.create(label='Foo bar', kind='events') |
894 | 938 |
event = Event.objects.create( |
tests/api/test_meetings_datetimes.py | ||
---|---|---|
2300 | 2300 |
assert ( |
2301 | 2301 |
False |
2302 | 2302 |
), 'slot should not appear due to maximal_booking_delay of the real agenda (and no maximal_booking_delay) is defined on the real agenda' |
2303 | ||
2304 | ||
2305 |
@pytest.mark.freeze_time('2022-01-20 14:00') # Thursday |
|
2306 |
def test_datetimes_api_meetings_agenda_weekday_indexes(app): |
|
2307 |
agenda = Agenda.objects.create( |
|
2308 |
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 |
|
2309 |
) |
|
2310 |
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30) |
|
2311 |
desk = Desk.objects.create(agenda=agenda, label='desk') |
|
2312 | ||
2313 |
time_period = TimePeriod.objects.create( |
|
2314 |
weekday=3, # Thursday |
|
2315 |
start_time=datetime.time(11, 0), |
|
2316 |
end_time=datetime.time(12, 0), |
|
2317 |
desk=desk, |
|
2318 |
) |
|
2319 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug) |
|
2320 | ||
2321 |
resp = app.get(api_url) |
|
2322 |
assert len(resp.json['data']) == 16 |
|
2323 |
assert [x['datetime'] for x in resp.json['data']][:6] == [ |
|
2324 |
'2022-01-27 11:00:00', |
|
2325 |
'2022-01-27 11:30:00', |
|
2326 |
'2022-02-03 11:00:00', |
|
2327 |
'2022-02-03 11:30:00', |
|
2328 |
'2022-02-10 11:00:00', |
|
2329 |
'2022-02-10 11:30:00', |
|
2330 |
] |
|
2331 |
every_weeks_resp = resp |
|
2332 | ||
2333 |
time_period.weekday_indexes = [1] |
|
2334 |
time_period.save() |
|
2335 |
resp = app.get(api_url) |
|
2336 |
assert len(resp.json['data']) == 4 |
|
2337 |
assert [x['datetime'] for x in resp.json['data']] == [ |
|
2338 |
'2022-02-03 11:00:00', |
|
2339 |
'2022-02-03 11:30:00', |
|
2340 |
'2022-03-03 11:00:00', |
|
2341 |
'2022-03-03 11:30:00', |
|
2342 |
] |
|
2343 | ||
2344 |
time_period.weekday_indexes = [1, 3] |
|
2345 |
time_period.save() |
|
2346 |
resp = app.get(api_url) |
|
2347 |
assert len(resp.json['data']) == 8 |
|
2348 |
assert [x['datetime'] for x in resp.json['data']] == [ |
|
2349 |
'2022-02-03 11:00:00', |
|
2350 |
'2022-02-03 11:30:00', |
|
2351 |
'2022-02-17 11:00:00', |
|
2352 |
'2022-02-17 11:30:00', |
|
2353 |
'2022-03-03 11:00:00', |
|
2354 |
'2022-03-03 11:30:00', |
|
2355 |
'2022-03-17 11:00:00', |
|
2356 |
'2022-03-17 11:30:00', |
|
2357 |
] |
|
2358 | ||
2359 |
time_period.weekday_indexes = [1, 2, 3, 4, 5] |
|
2360 |
time_period.save() |
|
2361 |
resp = app.get(api_url) |
|
2362 |
assert resp.json == every_weeks_resp.json |
|
2363 | ||
2364 |
# there are five Mondays this month |
|
2365 |
time_period.weekday = 0 |
|
2366 |
time_period.weekday_indexes = [5] |
|
2367 |
time_period.save() |
|
2368 |
resp = app.get(api_url) |
|
2369 |
assert len(resp.json['data']) == 2 |
|
2370 |
assert [x['datetime'] for x in resp.json['data']] == ['2022-01-31 11:00:00', '2022-01-31 11:30:00'] |
tests/test_time_periods.py | ||
---|---|---|
255 | 255 |
assert events[2].timetuple()[:5] == (2016, 9, 19, 21, 0) |
256 | 256 |
assert events[3].timetuple()[:5] == (2016, 9, 26, 21, 0) |
257 | 257 |
assert len(events) == 4 |
258 | ||
259 | ||
260 |
def test_timeperiod_weekday_indexes(): |
|
261 |
agenda = Agenda.objects.create( |
|
262 |
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=60 |
|
263 |
) |
|
264 |
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=60) |
|
265 |
desk = Desk.objects.create(agenda=agenda, label='desk') |
|
266 | ||
267 |
timeperiod = TimePeriod.objects.create( |
|
268 |
weekday=0, # Monday |
|
269 |
start_time=datetime.time(11, 0), |
|
270 |
end_time=datetime.time(12, 0), |
|
271 |
desk=desk, |
|
272 |
) |
|
273 | ||
274 |
def get_events(min_datetime, max_datetime): |
|
275 |
return sorted( |
|
276 |
timeperiod.as_shared_timeperiods().get_time_slots( |
|
277 |
min_datetime=make_aware(min_datetime), |
|
278 |
max_datetime=make_aware(max_datetime), |
|
279 |
meeting_duration=meeting_type.duration, |
|
280 |
base_duration=agenda.get_base_meeting_duration(), |
|
281 |
) |
|
282 |
) |
|
283 | ||
284 |
events = get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) |
|
285 |
assert events[0].timetuple()[:5] == (2022, 3, 7, 11, 0) |
|
286 |
assert events[1].timetuple()[:5] == (2022, 3, 14, 11, 0) |
|
287 |
assert events[2].timetuple()[:5] == (2022, 3, 21, 11, 0) |
|
288 |
assert events[3].timetuple()[:5] == (2022, 3, 28, 11, 0) |
|
289 |
assert len(events) == 4 |
|
290 | ||
291 |
timeperiod.weekday_indexes = [1] |
|
292 |
timeperiod.save() |
|
293 |
events = get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) |
|
294 |
assert events[0].timetuple()[:5] == (2022, 3, 7, 11, 0) |
|
295 |
assert len(events) == 1 |
|
296 | ||
297 |
timeperiod.weekday_indexes = [3, 4] |
|
298 |
timeperiod.save() |
|
299 |
events = get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) |
|
300 |
assert events[0].timetuple()[:5] == (2022, 3, 21, 11, 0) |
|
301 |
assert events[1].timetuple()[:5] == (2022, 3, 28, 11, 0) |
|
302 |
assert len(events) == 2 |
|
303 | ||
304 |
timeperiod.weekday_indexes = [5] |
|
305 |
timeperiod.save() |
|
306 |
assert get_events(datetime.datetime(2022, 3, 1), datetime.datetime(2022, 4, 1)) == [] |
|
307 | ||
308 |
# month with five Mondays |
|
309 |
events = get_events(datetime.datetime(2022, 5, 1), datetime.datetime(2022, 6, 1)) |
|
310 |
assert events[0].timetuple()[:5] == (2022, 5, 30, 11, 0) |
|
311 |
assert len(events) == 1 |
|
312 | ||
313 |
# reduce ranges |
|
314 |
events = get_events(datetime.datetime(2022, 5, 30), datetime.datetime(2022, 5, 31)) |
|
315 |
assert events[0].timetuple()[:5] == (2022, 5, 30, 11, 0) |
|
316 |
assert len(events) == 1 |
|
317 | ||
318 |
assert get_events(datetime.datetime(2022, 5, 29), datetime.datetime(2022, 5, 30)) == [] |
|
319 |
assert get_events(datetime.datetime(2022, 5, 1), datetime.datetime(2022, 5, 20)) == [] |
|
320 | ||
321 |
# midnight overlap |
|
322 |
timeperiod.start_time = datetime.time(22, 0) |
|
323 |
timeperiod.end_time = datetime.time(23, 0) |
|
324 |
timeperiod.save() |
|
325 | ||
326 |
events = get_events(datetime.datetime(2022, 5, 1), datetime.datetime(2022, 6, 1)) |
|
327 |
assert events[0].timetuple()[:5] == (2022, 5, 30, 22, 0) |
|
328 |
assert len(events) == 1 |
|
258 |
- |