From ce3bd26ebc823e9c5b8de6dc1e58efdebd29db44 Mon Sep 17 00:00:00 2001 From: Diego Cepeda Date: Tue, 12 Mar 2024 13:48:48 -0500 Subject: [PATCH] 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 --- .circleci/config.yml | 2 +- CHANGELOG.md | 8 +++++ db/schema.v0.sql | 6 ++-- e2e/test_populate.py | 51 ++++++++++++++++++++++++++++++ src/oncall/__init__.py | 2 +- src/oncall/scheduler/multi-team.py | 24 ++++++++++++++ src/oncall/ui/static/js/oncall.js | 3 ++ src/oncall/ui/templates/index.html | 6 ++++ 8 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 src/oncall/scheduler/multi-team.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 80bd1da9..fb9a7bad 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 908de56f..210c7e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/db/schema.v0.sql b/db/schema.v0.sql index 9afdc60a..6385598b 100644 --- a/db/schema.v0.sql +++ b/db/schema.v0.sql @@ -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 diff --git a/e2e/test_populate.py b/e2e/test_populate.py index 93c094e4..5300dc60 100644 --- a/e2e/test_populate.py +++ b/e2e/test_populate.py @@ -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() diff --git a/src/oncall/__init__.py b/src/oncall/__init__.py index 0b167e61..edc60b35 100644 --- a/src/oncall/__init__.py +++ b/src/oncall/__init__.py @@ -1 +1 @@ -__version__ = "2.1.5" +__version__ = "2.1.6" diff --git a/src/oncall/scheduler/multi-team.py b/src/oncall/scheduler/multi-team.py new file mode 100644 index 00000000..20dbe379 --- /dev/null +++ b/src/oncall/scheduler/multi-team.py @@ -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()] \ No newline at end of file diff --git a/src/oncall/ui/static/js/oncall.js b/src/oncall/ui/static/js/oncall.js index 785f6e93..46122fc1 100644 --- a/src/oncall/ui/static/js/oncall.js +++ b/src/oncall/ui/static/js/oncall.js @@ -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; }); diff --git a/src/oncall/ui/templates/index.html b/src/oncall/ui/templates/index.html index 3fe5abd2..70414419 100644 --- a/src/oncall/ui/templates/index.html +++ b/src/oncall/ui/templates/index.html @@ -1121,6 +1121,7 @@

+
@@ -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. + + +