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
Show file tree
Hide file tree
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
Expand Up @@ -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 = {
Expand Down
28 changes: 21 additions & 7 deletions src/oncall/scheduler/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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']
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/oncall/ui/static/js/oncall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
35 changes: 22 additions & 13 deletions src/oncall/ui/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
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
Expand Up @@ -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
Expand Down