Skip to content

Commit ce3bd26

Browse files
authored
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
1 parent 88ae866 commit ce3bd26

File tree

8 files changed

+98
-4
lines changed

8 files changed

+98
-4
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: 2
22
jobs:
33
build:
44
docker:
5-
- image: cimg/python:3.10.8-browsers
5+
- image: cimg/python:3.10.11
66
- image: mysql/mysql-server:8.0
77
environment:
88
- MYSQL_ROOT_PASSWORD=1234

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4+
## [2.1.6] - 2024-03-11
5+
6+
### Added
7+
- 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
8+
### Changed
9+
10+
### Fixed
11+
412

513
## [2.0.0] - 2023-06-06
614
WARNING: this version adds a change to the MYSQL schema! Make changes to the schema before deploying new 2.0.0 version.

db/schema.v0.sql

+4-2
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,10 @@ VALUES ('default',
480480
'Default scheduling algorithm'),
481481
('round-robin',
482482
'Round robin in roster order; does not respect vacations/conflicts'),
483-
('no-skip-matching',
484-
'Default scheduling algorithm; doesn\'t skips creating events if matching events already exist on the calendar');
483+
('no-skip-matching',
484+
'Default scheduling algorithm; doesn\'t skips creating events if matching events already exist on the calendar'),
485+
('multi-team',
486+
'Allows multiple role events. Prevents scheduling if there are any conflicting events even across teams.');
485487

486488
-- -----------------------------------------------------
487489
-- Initialize notification types

e2e/test_populate.py

+51
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,57 @@ def test_v0_populate_vacation_propagate(user, team, roster, role, schedule, even
107107
assert len(events) == 2
108108
assert events[0]['user'] == events[1]['user'] == user_name_2
109109

110+
111+
@prefix('test_v0_populate_vacation_propagate')
112+
def test_v0_populate_multi_team(user, team, roster, role, schedule, event):
113+
user_name = user.create()
114+
user_name_2 = user.create()
115+
team_name = team.create()
116+
team_name_2 = team.create()
117+
role_name = role.create()
118+
roster_name = roster.create(team_name)
119+
schedule_id = schedule.create(team_name,
120+
roster_name,
121+
{'role': role_name,
122+
'events': [{'start': 0, 'duration': 604800}],
123+
'advanced_mode': 0,
124+
'auto_populate_threshold': 14,
125+
'scheduler': {'name': 'multi-team', 'data': []}})
126+
user.add_to_roster(user_name, team_name, roster_name)
127+
user.add_to_roster(user_name_2, team_name, roster_name)
128+
user.add_to_team(user_name, team_name_2)
129+
130+
# Populate for team 1
131+
re = requests.post(api_v0('schedules/%s/populate' % schedule_id), json = {'start': time.time()})
132+
assert re.status_code == 200
133+
134+
# Create conflicting primary event in team 2 for user 1
135+
re = requests.get(api_v0('events?team=%s' % team_name))
136+
assert re.status_code == 200
137+
events = re.json()
138+
assert len(events) == 2
139+
assert events[0]['user'] != events[1]['user']
140+
for e in events:
141+
event.create({
142+
'start': e['start'],
143+
'end': e['end'],
144+
'user': user_name,
145+
'team': team_name_2,
146+
'role': "primary",
147+
})
148+
149+
# Populate again for team 1
150+
re = requests.post(api_v0('schedules/%s/populate' % schedule_id), json = {'start': time.time()})
151+
assert re.status_code == 200
152+
153+
# Ensure events are both for user 2 (since user 1 is busy in team 2)
154+
re = requests.get(api_v0('events?team=%s&include_subscribed=false' % team_name))
155+
assert re.status_code == 200
156+
events = re.json()
157+
assert len(events) == 2
158+
assert events[0]['user'] == events[1]['user'] == user_name_2
159+
160+
110161
@prefix('test_v0_populate_over')
111162
def test_api_v0_populate_over(user, team, roster, role, schedule):
112163
user_name = user.create()

src/oncall/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.1.5"
1+
__version__ = "2.1.6"

src/oncall/scheduler/multi-team.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from . import default
2+
3+
4+
class Scheduler(default.Scheduler):
5+
# same as no-skip-matching
6+
def create_events(self, team_id, schedule_id, user_id, events, role_id, cursor, table_name='event', skip_match=True):
7+
super(Scheduler, self).create_events(team_id, schedule_id, user_id, events, role_id, cursor, table_name, skip_match=False)
8+
9+
def get_busy_user_by_event_range(self, user_ids, team_id, events, cursor, table_name='event'):
10+
''' Find which users have overlapping events for the same team in this time range'''
11+
query_params = [user_ids]
12+
range_check = []
13+
for e in events:
14+
range_check.append('(%s < `end` AND `start` < %s)')
15+
query_params += [e['start'], e['end']]
16+
17+
# in multi-team prevent a user being scheduled if they are already scheduled for any role in any team during the same time slot
18+
query = '''
19+
SELECT DISTINCT `user_id` FROM `%s`
20+
WHERE `user_id` in %%s AND (%s)
21+
''' % (table_name, ' OR '.join(range_check))
22+
23+
cursor.execute(query, query_params)
24+
return [r['user_id'] for r in cursor.fetchall()]

src/oncall/ui/static/js/oncall.js

+3
Original file line numberDiff line numberDiff line change
@@ -1766,6 +1766,7 @@ var oncall = {
17661766
'default': $('#default-scheduler-template').html(),
17671767
'round-robin': $('#round-robin-scheduler-template').html(),
17681768
'no-skip-matching': $('#allow-duplicate-scheduler-template').html(),
1769+
'multi-team': $('#multi-team-template').html(),
17691770
},
17701771
schedulerTypeContainer: '.scheduler-type-container',
17711772
schedulesUrl: '/api/v0/schedules/',
@@ -3246,6 +3247,8 @@ var oncall = {
32463247
Handlebars.registerHelper('friendlyScheduler', function(str){
32473248
if (str ==='no-skip-matching') {
32483249
return 'Default (allow duplicate)';
3250+
} else if (str ==='multi-team') {
3251+
return 'Default (multi-team aware)';
32493252
}
32503253
return str;
32513254
});

src/oncall/ui/templates/index.html

+6
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,7 @@ <h4>
11211121
<option value="default" {{isSelected 'default' selected_schedule.scheduler.name}}> Default </option>
11221122
<option value="round-robin" {{isSelected 'round-robin' selected_schedule.scheduler.name}}> Round-robin </option>
11231123
<option value="no-skip-matching" {{isSelected 'no-skip-matching' selected_schedule.scheduler.name}}> Default (allow duplicate) </option>
1124+
<option value="multi-team" {{isSelected 'multi-team' selected_schedule.scheduler.name}}> Default (multi-team aware) </option>
11241125
</select>
11251126
<div class="scheduler-type-container light">
11261127
<!-- scheduler specific data renders here -->
@@ -1229,6 +1230,11 @@ <h4>
12291230
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.
12301231
</script>
12311232

1233+
<!-- allow-duplicate scheduler template -->
1234+
<script id="multi-team-template" type="text/x-handlebars-template">
1235+
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.
1236+
</script>
1237+
12321238
<!--// **********************
12331239
Team.info Page
12341240
*********************** //-->

0 commit comments

Comments
 (0)