From 40fab26680997e48535536ccc1af5177e6f01024 Mon Sep 17 00:00:00 2001 From: Marius Cotofana Date: Mon, 26 Apr 2021 15:26:03 +0200 Subject: [PATCH] First version of daily rotations --- src/oncall/api/v0/schedules.py | 3 ++- src/oncall/scheduler/default.py | 28 ++++++++++++++++++------ src/oncall/ui/static/js/oncall.js | 1 + src/oncall/ui/templates/index.html | 35 +++++++++++++++++++----------- test/__init__.py | 0 test/test_scheduler.py | 31 ++++++++++++++++++++++++++ 6 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 test/__init__.py diff --git a/src/oncall/api/v0/schedules.py b/src/oncall/api/v0/schedules.py index a6cd4245..78c4d803 100644 --- a/src/oncall/api/v0/schedules.py +++ b/src/oncall/api/v0/schedules.py @@ -10,8 +10,9 @@ from ... import db HOUR = 60 * 60 +DAY = 24 * HOUR WEEK = 24 * HOUR * 7 -simple_ev_lengths = set([WEEK, 2 * WEEK]) +simple_ev_lengths = set([DAY, WEEK, 2 * WEEK]) simple_12hr_num_events = set([7, 14]) columns = { diff --git a/src/oncall/scheduler/default.py b/src/oncall/scheduler/default.py index 77c2207b..e8a3ffa1 100644 --- a/src/oncall/scheduler/default.py +++ b/src/oncall/scheduler/default.py @@ -236,6 +236,16 @@ def generate_events(self, schedule, schedule_events, epoch): generated.append({'start': start, 'end': end}) return generated + def get_period_in_days(self, schedule): + ''' + Find schedule rotation period in days + ''' + events = schedule['events'] + first_event = min(events, key=operator.itemgetter('start')) + end = max(e['start'] + e['duration'] for e in events) + period = end - first_event['start'] + return ((period + SECONDS_IN_A_DAY - 1) // SECONDS_IN_A_DAY) + def get_period_len(self, schedule): ''' Find schedule rotation period in weeks, rounded up @@ -247,7 +257,7 @@ def get_period_len(self, schedule): return ((period + SECONDS_IN_A_WEEK - 1) // SECONDS_IN_A_WEEK) def calculate_future_events(self, schedule, cursor, start_epoch=None): - period = self.get_period_len(schedule) + period = self.get_period_in_days(schedule) # DEFINITION: # epoch: Sunday at 00:00:00 in the schedule's local timezone. This is our point of reference when @@ -267,7 +277,7 @@ def calculate_future_events(self, schedule, cursor, start_epoch=None): # epoch and work from there) last_epoch_dt = datetime.fromtimestamp(last_epoch_timestamp, utc) localized_last_epoch = last_epoch_dt.astimezone(timezone(schedule['timezone'])) - next_epoch = self.get_closest_epoch(localized_last_epoch) + timedelta(days=7 * period) + next_epoch = self.get_closest_epoch(localized_last_epoch) + timedelta(days=period) else: next_epoch = start_epoch @@ -277,11 +287,11 @@ def calculate_future_events(self, schedule, cursor, start_epoch=None): # Start scheduling from the next epoch while cutoff_date > next_epoch: epoch_events = self.generate_events(schedule, schedule['events'], next_epoch) - next_epoch += timedelta(days=7 * period) + next_epoch += timedelta(days=period) if epoch_events: future_events.append(epoch_events) # Return future events and the last epoch events were scheduled for. - return future_events, self.utc_from_naive_date(next_epoch - timedelta(days=7 * period), schedule) + return future_events, self.utc_from_naive_date(next_epoch - timedelta(days=period), schedule) def find_next_user_id(self, schedule, future_events, cursor, table_name='event'): team_id = schedule['team_id'] @@ -377,14 +387,18 @@ def populate(self, schedule, start_time, dbinfo, table_name='event'): role_id = schedule['role_id'] team_id = schedule['team_id'] first_event_start = min(ev['start'] for ev in schedule['events']) - period = self.get_period_len(schedule) + period = self.get_period_in_days(schedule) handoff = start_epoch + timedelta(seconds=first_event_start) handoff = timezone(schedule['timezone']).localize(handoff) # Start scheduling from the next occurrence of the hand-off time. if start_dt > handoff: - start_epoch += timedelta(weeks=period) - handoff += timedelta(weeks=period) + if period < 7: # Need to add min 1 week so we can find the next occurance of the day + start_epoch += timedelta(weeks=1) + handoff += timedelta(weeks=1) + else: + start_epoch += timedelta(days=period) + handoff += timedelta(days=period) if handoff < utc.localize(datetime.utcnow()): cursor.execute("DROP TEMPORARY TABLE IF EXISTS `temp_event`") connection.commit() diff --git a/src/oncall/ui/static/js/oncall.js b/src/oncall/ui/static/js/oncall.js index 022985e2..ffc4df4a 100644 --- a/src/oncall/ui/static/js/oncall.js +++ b/src/oncall/ui/static/js/oncall.js @@ -1803,6 +1803,7 @@ var oncall = { var item = data.rosters[i]; for (var k = 0; k < item.schedules.length; k++) { var schedule = item.schedules[k]; + schedule.is_daily_rota = schedule.events.length > 0 && schedule.events[0].duration * 1000 === msPerDay; schedule.is_12_hr = !schedule.advanced_mode && schedule.events.length > 1; for (var j = 0, eventItem; j < schedule.events.length; j++) { eventItem = schedule.events[j]; diff --git a/src/oncall/ui/templates/index.html b/src/oncall/ui/templates/index.html index ec0bcafa..81b6b056 100644 --- a/src/oncall/ui/templates/index.html +++ b/src/oncall/ui/templates/index.html @@ -969,23 +969,31 @@
  • @@ -1158,6 +1166,7 @@


    diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_scheduler.py b/test/test_scheduler.py index 1882d1c6..65c44797 100644 --- a/test/test_scheduler.py +++ b/test/test_scheduler.py @@ -25,6 +25,37 @@ def test_find_new_user_as_least_active_user(mocker): user_id = scheduler.find_next_user_id(MOCK_SCHEDULE, [{'start': 0, 'end': 5}], None) assert user_id == 123 +def test_calculate_future_events_1_24_shifts(mocker): + mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = None + mock_dt = datetime.datetime(year=2017, month=2, day=7, hour=10) + mocker.patch('time.time').return_value = time.mktime(mock_dt.timetuple()) + start = DAY + 10 * HOUR + 30 * MIN # Monday at 10:30 am + schedule_foo = { + 'timezone': 'US/Pacific', + 'auto_populate_threshold': 7, + 'events': [{ + 'start': start, # 24hr daily shift starting Monday at 10:30 am + 'duration': DAY + }] + } + scheduler = oncall.scheduler.default.Scheduler() + future_events, last_epoch = scheduler.calculate_future_events(schedule_foo, None) + assert len(future_events) == 10 + + mondays = (6, 13) + for i, epoch in enumerate(future_events): + assert len(epoch) == 1 + ev = epoch[0] + start_dt = utc.localize(datetime.datetime.utcfromtimestamp(ev['start'])) + start_dt = start_dt.astimezone(timezone('US/Pacific')) + assert start_dt.timetuple().tm_year == mock_dt.timetuple().tm_year + assert start_dt.timetuple().tm_mon == mock_dt.timetuple().tm_mon + assert start_dt.timetuple().tm_mday == 6 + i + assert start_dt.timetuple().tm_wday == i % 7 + assert start_dt.timetuple().tm_hour == 10 # 10: + assert start_dt.timetuple().tm_min == 30 # 30 am + assert start_dt.timetuple().tm_sec == 00 + assert ev['end'] - ev['start'] == DAY def test_calculate_future_events_7_24_shifts(mocker): mocker.patch('oncall.scheduler.default.Scheduler.get_schedule_last_epoch').return_value = None