Skip to content

Commit

Permalink
New multi-team scheduler (linkedin#415)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
diegocepedaw authored Mar 12, 2024
1 parent 88ae866 commit ce3bd26
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
6 changes: 4 additions & 2 deletions db/schema.v0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,10 @@ VALUES ('default',
'Default scheduling algorithm'),
('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');
('no-skip-matching',
'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
Expand Down
51 changes: 51 additions & 0 deletions e2e/test_populate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/oncall/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.1.5"
__version__ = "2.1.6"
24 changes: 24 additions & 0 deletions src/oncall/scheduler/multi-team.py
Original file line number Diff line number Diff line change
@@ -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()]
3 changes: 3 additions & 0 deletions src/oncall/ui/static/js/oncall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/',
Expand Down Expand Up @@ -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;
});
Expand Down
6 changes: 6 additions & 0 deletions src/oncall/ui/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,7 @@ <h4>
<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 -->
Expand Down Expand Up @@ -1229,6 +1230,11 @@ <h4>
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
*********************** //-->
Expand Down

0 comments on commit ce3bd26

Please sign in to comment.