Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable daily rotations #348

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/oncall/api/v0/schedules.py
Original file line number Diff line number Diff line change
@@ -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 = {
28 changes: 21 additions & 7 deletions src/oncall/scheduler/default.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions src/oncall/ui/static/js/oncall.js
Original file line number Diff line number Diff line change
@@ -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];
35 changes: 22 additions & 13 deletions src/oncall/ui/templates/index.html
Original file line number Diff line number Diff line change
@@ -969,23 +969,31 @@ <h4 class="modal-title">Populate Schedule</h4>
<li>
<label class="light label-col">Rotation:</label>
<ul class="data-col schedule-rotation">
{{#if is_12_hr}}
{{#isEqual events.length 7}}
{{timeSince events.[0].start 'toString'}} - Weekly (12 hr)
{{#if is_daily_rota}}
{{#if is_12_hr}}
{{timeSince events.[0].start 'toString'}} - Daily (12 hr)
{{else}}
{{timeSince events.[0].start 'toString'}} - Bi-Weekly (12 hr)
{{/isEqual}}
{{timeSince events.[0].start 'toString'}} - Daily
{{/if}}
{{else}}
{{#if advanced_mode}}
{{#each events}}
{{timeSince start 'toString'}} - {{timeSince end 'toString'}}<br />
{{/each}}
{{else}}
{{#isEqual events.[0].duration 604800000}}
{{timeSince events.[0].start 'toString'}} - Weekly
{{#if is_12_hr}}
{{#isEqual events.length 7}}
{{timeSince events.[0].start 'toString'}} - Weekly (12 hr)
{{else}}
{{timeSince events.[0].start 'toString'}} - Bi-Weekly
{{timeSince events.[0].start 'toString'}} - Bi-Weekly (12 hr)
{{/isEqual}}
{{else}}
{{#if advanced_mode}}
{{#each events}}
{{timeSince start 'toString'}} - {{timeSince end 'toString'}}<br />
{{/each}}
{{else}}
{{#isEqual events.[0].duration 604800000}}
{{timeSince events.[0].start 'toString'}} - Weekly
{{else}}
{{timeSince events.[0].start 'toString'}} - Bi-Weekly
{{/isEqual}}
{{/if}}
{{/if}}
{{/if}}
</ul>
@@ -1158,6 +1166,7 @@ <h4>
<label>Rotate:</label>
<br>
<select name="time" class="form-control rotation-end-duration">
<option value="1" {{#if totalEvents}} {{isSelected totalEvents 1}} {{else}} {{isSelected duration 1}} {{/if}}>Daily</option>
<option value="7" {{#if totalEvents}} {{isSelected totalEvents 7}} {{else}} {{isSelected duration 7}} {{/if}}>Weekly</option>
<option value="14" {{#if totalEvents}} {{isSelected totalEvents 14}} {{else}} {{isSelected duration 14}} {{/if}}>Bi-Weekly</option>
</select>
Empty file added test/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions test/test_scheduler.py
Original file line number Diff line number Diff line change
@@ -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