You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-27 23:18:38 +02:00
New multi-team scheduler (#415)
* allow editing info for api managed teams * add a team description field [MYSQL SCHEMA CHANGE] * modify tests [MYSQL SCHEMA CHANGE] * add multi-team scheduler * use py image * add changelog * py img * fix typo * add test
This commit is contained in:
@@ -2,7 +2,7 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/python:3.10.8-browsers
|
||||
- image: cimg/python:3.10.11
|
||||
- image: mysql/mysql-server:8.0
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=1234
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.1.6] - 2024-03-11
|
||||
|
||||
### Added
|
||||
- New multi-team scheduler type which allows checking all teams for potential scheduling conficts when scheduling events. The new multi-team schema should be inserted into the `schema` table as shown in db/schema.v0.sql
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
## [2.0.0] - 2023-06-06
|
||||
WARNING: this version adds a change to the MYSQL schema! Make changes to the schema before deploying new 2.0.0 version.
|
||||
|
||||
@@ -481,7 +481,9 @@ VALUES ('default',
|
||||
('round-robin',
|
||||
'Round robin in roster order; does not respect vacations/conflicts'),
|
||||
('no-skip-matching',
|
||||
'Default scheduling algorithm; doesn\'t skips creating events if matching events already exist on the calendar');
|
||||
'Default scheduling algorithm; doesn\'t skips creating events if matching events already exist on the calendar'),
|
||||
('multi-team',
|
||||
'Allows multiple role events. Prevents scheduling if there are any conflicting events even across teams.');
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- Initialize notification types
|
||||
|
||||
@@ -107,6 +107,57 @@ def test_v0_populate_vacation_propagate(user, team, roster, role, schedule, even
|
||||
assert len(events) == 2
|
||||
assert events[0]['user'] == events[1]['user'] == user_name_2
|
||||
|
||||
|
||||
@prefix('test_v0_populate_vacation_propagate')
|
||||
def test_v0_populate_multi_team(user, team, roster, role, schedule, event):
|
||||
user_name = user.create()
|
||||
user_name_2 = user.create()
|
||||
team_name = team.create()
|
||||
team_name_2 = team.create()
|
||||
role_name = role.create()
|
||||
roster_name = roster.create(team_name)
|
||||
schedule_id = schedule.create(team_name,
|
||||
roster_name,
|
||||
{'role': role_name,
|
||||
'events': [{'start': 0, 'duration': 604800}],
|
||||
'advanced_mode': 0,
|
||||
'auto_populate_threshold': 14,
|
||||
'scheduler': {'name': 'multi-team', 'data': []}})
|
||||
user.add_to_roster(user_name, team_name, roster_name)
|
||||
user.add_to_roster(user_name_2, team_name, roster_name)
|
||||
user.add_to_team(user_name, team_name_2)
|
||||
|
||||
# Populate for team 1
|
||||
re = requests.post(api_v0('schedules/%s/populate' % schedule_id), json = {'start': time.time()})
|
||||
assert re.status_code == 200
|
||||
|
||||
# Create conflicting primary event in team 2 for user 1
|
||||
re = requests.get(api_v0('events?team=%s' % team_name))
|
||||
assert re.status_code == 200
|
||||
events = re.json()
|
||||
assert len(events) == 2
|
||||
assert events[0]['user'] != events[1]['user']
|
||||
for e in events:
|
||||
event.create({
|
||||
'start': e['start'],
|
||||
'end': e['end'],
|
||||
'user': user_name,
|
||||
'team': team_name_2,
|
||||
'role': "primary",
|
||||
})
|
||||
|
||||
# Populate again for team 1
|
||||
re = requests.post(api_v0('schedules/%s/populate' % schedule_id), json = {'start': time.time()})
|
||||
assert re.status_code == 200
|
||||
|
||||
# Ensure events are both for user 2 (since user 1 is busy in team 2)
|
||||
re = requests.get(api_v0('events?team=%s&include_subscribed=false' % team_name))
|
||||
assert re.status_code == 200
|
||||
events = re.json()
|
||||
assert len(events) == 2
|
||||
assert events[0]['user'] == events[1]['user'] == user_name_2
|
||||
|
||||
|
||||
@prefix('test_v0_populate_over')
|
||||
def test_api_v0_populate_over(user, team, roster, role, schedule):
|
||||
user_name = user.create()
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.1.5"
|
||||
__version__ = "2.1.6"
|
||||
|
||||
24
src/oncall/scheduler/multi-team.py
Normal file
24
src/oncall/scheduler/multi-team.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from . import default
|
||||
|
||||
|
||||
class Scheduler(default.Scheduler):
|
||||
# same as no-skip-matching
|
||||
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, table_name='event', skip_match=True):
|
||||
super(Scheduler, self).create_events(team_id, schedule_id, user_id, events, role_id, cursor, table_name, skip_match=False)
|
||||
|
||||
def get_busy_user_by_event_range(self, user_ids, team_id, events, cursor, table_name='event'):
|
||||
''' Find which users have overlapping events for the same team in this time range'''
|
||||
query_params = [user_ids]
|
||||
range_check = []
|
||||
for e in events:
|
||||
range_check.append('(%s < `end` AND `start` < %s)')
|
||||
query_params += [e['start'], e['end']]
|
||||
|
||||
# in multi-team prevent a user being scheduled if they are already scheduled for any role in any team during the same time slot
|
||||
query = '''
|
||||
SELECT DISTINCT `user_id` FROM `%s`
|
||||
WHERE `user_id` in %%s AND (%s)
|
||||
''' % (table_name, ' OR '.join(range_check))
|
||||
|
||||
cursor.execute(query, query_params)
|
||||
return [r['user_id'] for r in cursor.fetchall()]
|
||||
@@ -1766,6 +1766,7 @@ var oncall = {
|
||||
'default': $('#default-scheduler-template').html(),
|
||||
'round-robin': $('#round-robin-scheduler-template').html(),
|
||||
'no-skip-matching': $('#allow-duplicate-scheduler-template').html(),
|
||||
'multi-team': $('#multi-team-template').html(),
|
||||
},
|
||||
schedulerTypeContainer: '.scheduler-type-container',
|
||||
schedulesUrl: '/api/v0/schedules/',
|
||||
@@ -3246,6 +3247,8 @@ var oncall = {
|
||||
Handlebars.registerHelper('friendlyScheduler', function(str){
|
||||
if (str ==='no-skip-matching') {
|
||||
return 'Default (allow duplicate)';
|
||||
} else if (str ==='multi-team') {
|
||||
return 'Default (multi-team aware)';
|
||||
}
|
||||
return str;
|
||||
});
|
||||
|
||||
@@ -1121,6 +1121,7 @@
|
||||
<option value="default" {{isSelected 'default' selected_schedule.scheduler.name}}> Default </option>
|
||||
<option value="round-robin" {{isSelected 'round-robin' selected_schedule.scheduler.name}}> Round-robin </option>
|
||||
<option value="no-skip-matching" {{isSelected 'no-skip-matching' selected_schedule.scheduler.name}}> Default (allow duplicate) </option>
|
||||
<option value="multi-team" {{isSelected 'multi-team' selected_schedule.scheduler.name}}> Default (multi-team aware) </option>
|
||||
</select>
|
||||
<div class="scheduler-type-container light">
|
||||
<!-- scheduler specific data renders here -->
|
||||
@@ -1229,6 +1230,11 @@
|
||||
The Default (allow duplicate) scheduler uses the same algorithm as Default, but allows more than one user to be on-call at the same time for a given role. This lets you have duplicate primary events across several schedule templates.
|
||||
</script>
|
||||
|
||||
<!-- allow-duplicate scheduler template -->
|
||||
<script id="multi-team-template" type="text/x-handlebars-template">
|
||||
The Default (multi-team aware) scheduler uses the same algorithm as Default, but allows more than one user to be on-call at the same time for a given role. Additionally when scheduling it will check for conflicting events across all teams.
|
||||
</script>
|
||||
|
||||
<!--// **********************
|
||||
Team.info Page
|
||||
*********************** //-->
|
||||
|
||||
Reference in New Issue
Block a user