diff --git a/db/dummy_data.sql b/db/dummy_data.sql index fa064f0..e0c878a 100644 --- a/db/dummy_data.sql +++ b/db/dummy_data.sql @@ -13,7 +13,7 @@ UNLOCK TABLES; LOCK TABLES `team` WRITE; /*!40000 ALTER TABLE `team` DISABLE KEYS */; -INSERT INTO `team` VALUES (1,'Test Team','#team','team@example.com','US/Pacific',1,NULL,0); +INSERT INTO `team` VALUES (1,'Test Team','#team','team@example.com','US/Pacific',1,NULL,0,NULL); /*!40000 ALTER TABLE `team` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/schema.v0.sql b/db/schema.v0.sql index 2fad857..e1b8541 100644 --- a/db/schema.v0.sql +++ b/db/schema.v0.sql @@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `team` ( `active` BOOLEAN NOT NULL DEFAULT TRUE, `iris_plan` VARCHAR(255), `iris_enabled` BOOLEAN NOT NULL DEFAULT FALSE, + `override_phone_number` VARCHAR(255), PRIMARY KEY (`id`), UNIQUE INDEX `name_unique` (`name` ASC)); diff --git a/e2e/conftest.py b/e2e/conftest.py index 3df9341..9184235 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -62,7 +62,7 @@ def user(request): @pytest.fixture(scope="function") -def team(request, user): +def team(request, user, service): class TeamFactory(object): @@ -239,4 +239,4 @@ def service(request): factory = ServiceFactory(request.function.prefix) yield factory - factory.cleanup() \ No newline at end of file + factory.cleanup() diff --git a/e2e/test_services.py b/e2e/test_services.py index 9a4594a..620f9d6 100644 --- a/e2e/test_services.py +++ b/e2e/test_services.py @@ -115,3 +115,38 @@ def test_api_v0_services_current_oncall(team, service, user, role, event): assert re.status_code == 200 results = re.json() assert len(results) == 2 + + +@prefix('test_v0_service_override_number') +def test_api_v0_service_override_number(team, user, role, event, service): + team_name = team.create() + user_name = user.create() + user_name_2 = user.create() + service_name = service.create() + user.add_to_team(user_name, team_name) + user.add_to_team(user_name_2, team_name) + + start, end = int(time.time()), int(time.time()+36000) + event_data_1 = {'start': start, + 'end': end, + 'user': user_name, + 'team': team_name, + 'role': 'primary'} + event.create(event_data_1) + + re = requests.post(api_v0('teams/%s/services' % team_name), + json={'name': service_name}) + override_num = '12345' + re = requests.put(api_v0('teams/'+team_name), json={'override_phone_number': override_num}) + + re = requests.get(api_v0('services/%s/oncall/%s' % (service_name, 'primary'))) + assert re.status_code == 200 + results = re.json() + assert results[0]['start'] == start + assert results[0]['end'] == end + assert results[0]['contacts']['call'] == override_num + + re = requests.get(api_v0('services/%s/oncall' % service_name)) + assert re.status_code == 200 + results = re.json() + assert results[0]['contacts']['call'] == override_num diff --git a/e2e/test_teams.py b/e2e/test_teams.py index c24fa95..08cea24 100644 --- a/e2e/test_teams.py +++ b/e2e/test_teams.py @@ -58,7 +58,8 @@ def test_api_v0_get_team(team, role, roster, schedule): assert re.status_code == 200 team = re.json() assert isinstance(team, dict) - expected_set = {'users', 'admins', 'services', 'rosters', 'name', 'id', 'slack_channel', 'email', 'scheduling_timezone', 'iris_plan', 'iris_enabled'} + expected_set = {'users', 'admins', 'services', 'rosters', 'name', 'id', 'slack_channel', 'email', + 'scheduling_timezone', 'iris_plan', 'iris_enabled', 'override_phone_number'} assert expected_set == set(team.keys()) # it should also support filter by fields @@ -66,7 +67,8 @@ def test_api_v0_get_team(team, role, roster, schedule): assert re.status_code == 200 team = re.json() assert isinstance(team, dict) - expected_set = {'users', 'admins', 'services', 'name', 'id', 'slack_channel', 'email', 'scheduling_timezone', 'iris_plan', 'iris_enabled'} + expected_set = {'users', 'admins', 'services', 'name', 'id', 'slack_channel', 'email', + 'scheduling_timezone', 'iris_plan', 'iris_enabled', 'override_phone_number'} assert expected_set == set(team.keys()) @@ -86,6 +88,7 @@ def test_api_v0_update_team(team): new_team_name = "new-moninfra-update" email = 'abc@gmail.com' slack = '#slack' + override_num = '1234' # setup DB state requests.delete(api_v0('teams/'+new_team_name)) @@ -95,7 +98,10 @@ def test_api_v0_update_team(team): re = requests.get(api_v0('teams/'+team_name)) assert re.status_code == 200 # edit team name/email/slack - re = requests.put(api_v0('teams/'+team_name), json={'name': new_team_name, 'email': email, 'slack_channel': slack}) + re = requests.put(api_v0('teams/'+team_name), json={'name': new_team_name, + 'email': email, + 'slack_channel': slack, + 'override_phone_number': override_num}) assert re.status_code == 200 team.mark_for_cleaning(new_team_name) # verify result @@ -106,6 +112,7 @@ def test_api_v0_update_team(team): data = re.json() assert data['email'] == email assert data['slack_channel'] == slack + assert data['override_phone_number'] == override_num @prefix('test_v0_team_admin') @@ -356,3 +363,38 @@ def test_api_v0_team_current_oncall(team, user, role, event): assert re.status_code == 200 results = re.json() assert len(results) == 2 + + +@prefix('test_v0_team_override_number') +def test_api_v0_team_override_number(team, user, role, event): + team_name = team.create() + user_name = user.create() + user_name_2 = user.create() + user.add_to_team(user_name, team_name) + user.add_to_team(user_name_2, team_name) + + start, end = int(time.time()), int(time.time()+36000) + event_data_1 = {'start': start, + 'end': end, + 'user': user_name, + 'team': team_name, + 'role': 'primary'} + event.create(event_data_1) + + override_num = '12345' + re = requests.put(api_v0('teams/'+team_name), json={'override_phone_number': override_num}) + + re = requests.get(api_v0('teams/%s/oncall/%s' % (team_name, 'primary'))) + assert re.status_code == 200 + results = re.json() + assert results[0]['start'] == start + assert results[0]['end'] == end + assert results[0]['contacts']['call'] == override_num + + re = requests.get(api_v0('teams/%s/oncall' % team_name)) + assert re.status_code == 200 + results = re.json() + assert results[0]['contacts']['call'] == override_num + + re = requests.get(api_v0('teams/%s/summary' % team_name)) + assert results[0]['contacts']['call'] == override_num diff --git a/src/oncall/api/v0/service_oncall.py b/src/oncall/api/v0/service_oncall.py index a3ca7ca..e4eaf57 100644 --- a/src/oncall/api/v0/service_oncall.py +++ b/src/oncall/api/v0/service_oncall.py @@ -63,11 +63,14 @@ def on_get(req, resp, service, role=None): connection = db.connect() cursor = connection.cursor(db.DictCursor) # Get subscription teams for teams owning the service, along with the teams that own the service - cursor.execute('''SELECT `team_id` FROM `team_service` + cursor.execute('''SELECT `team_id`, `team`.`override_phone_number`, `team`.`name` FROM `team_service` JOIN `service` ON `service`.`id` = `team_service`.`service_id` + JOIN `team` ON `team`.`id` = `team_service`.`team_id` WHERE `service`.`name` = %s''', service) - team_ids = [row['team_id'] for row in cursor] + data = cursor.fetchall() + team_ids = [row['team_id'] for row in data] + team_override_numbers = {row['name']: row['override_phone_number'] for row in data} if not team_ids: resp.body = json_dumps([]) cursor.close() @@ -92,6 +95,11 @@ def on_get(req, resp, service, role=None): dest = row.pop('destination') ret[user]['contacts'][mode] = dest data = ret.values() + for event in data: + override_number = team_override_numbers.get(event['team']) + if override_number and event['role'] == 'primary': + event['contacts']['call'] = override_number + event['contacts']['sms'] = override_number cursor.close() connection.close() diff --git a/src/oncall/api/v0/team.py b/src/oncall/api/v0/team.py index 8942967..c323384 100644 --- a/src/oncall/api/v0/team.py +++ b/src/oncall/api/v0/team.py @@ -13,7 +13,8 @@ from ...utils import load_json_body, invalid_char_reg, create_audit from ...constants import TEAM_DELETED, TEAM_EDITED -cols = set(['name', 'slack_channel', 'email', 'scheduling_timezone', 'iris_plan', 'iris_enabled']) +cols = set(['name', 'slack_channel', 'email', 'scheduling_timezone', 'iris_plan', 'iris_enabled', + 'override_phone_number']) def populate_team_users(cursor, team_dict): @@ -145,7 +146,7 @@ def on_get(req, resp, team): connection = db.connect() cursor = connection.cursor(db.DictCursor) - cursor.execute('SELECT `id`, `name`, `email`, `slack_channel`, `scheduling_timezone`, `iris_plan`, `iris_enabled` ' + cursor.execute('SELECT `id`, `name`, `email`, `slack_channel`, `scheduling_timezone`, `iris_plan`, `iris_enabled`, `override_phone_number` ' 'FROM `team` WHERE `name`=%s AND `active` = %s', (team, active)) results = cursor.fetchall() if not results: @@ -203,7 +204,7 @@ def on_put(req, resp, team): raise HTTPBadRequest('invalid team name', 'team name contains invalid character "%s"' % invalid_char.group()) - if 'iris_plan' in data: + if 'iris_plan' in data and data['iris_plan']: iris_plan = data['iris_plan'] plan_resp = iris.client.get(iris.client.url + 'plans?name=%s&active=1' % iris_plan) if plan_resp.status_code != 200 or plan_resp.json() == []: diff --git a/src/oncall/api/v0/team_oncall.py b/src/oncall/api/v0/team_oncall.py index 2f4b9a5..78b8d29 100644 --- a/src/oncall/api/v0/team_oncall.py +++ b/src/oncall/api/v0/team_oncall.py @@ -80,6 +80,9 @@ def on_get(req, resp, team, role=None): cursor = connection.cursor(db.DictCursor) cursor.execute(get_oncall_query, query_params) data = cursor.fetchall() + cursor.execute('SELECT `override_phone_number` FROM team WHERE `name` = %s', team) + team = cursor.fetchone() + override_number = team['override_phone_number'] if team else None ret = {} for row in data: user = row['user'] @@ -93,6 +96,10 @@ def on_get(req, resp, team, role=None): dest = row.pop('destination') ret[user]['contacts'][mode] = dest data = ret.values() + for event in data: + if override_number and event['role'] == 'primary': + event['contacts']['call'] = override_number + event['contacts']['sms'] = override_number cursor.close() connection.close() diff --git a/src/oncall/api/v0/team_summary.py b/src/oncall/api/v0/team_summary.py index adc8961..fb05ec2 100644 --- a/src/oncall/api/v0/team_summary.py +++ b/src/oncall/api/v0/team_summary.py @@ -110,10 +110,12 @@ def on_get(req, resp, team): connection = db.connect() cursor = connection.cursor(db.DictCursor) - cursor.execute('SELECT `id` FROM `team` WHERE `name` = %s', team) + cursor.execute('SELECT `id`, `override_phone_number` FROM `team` WHERE `name` = %s', team) if cursor.rowcount < 1: raise HTTPNotFound() - team_id = cursor.fetchone()['id'] + data = cursor.fetchone() + team_id = data['id'] + override_num = data['override_phone_number'] current_query = ''' SELECT `user`.`full_name` AS `full_name`, `user`.`photo_url`, @@ -196,5 +198,13 @@ def on_get(req, resp, team): cursor.close() connection.close() + if override_num is not None: + try: + for event in payload['current']['primary']: + event['user_contacts']['call'] = override_num + event['user_contacts']['sms'] = override_num + except KeyError: + # No current primary events exist, do nothing + pass resp.body = dumps(payload) diff --git a/src/oncall/api/v0/teams.py b/src/oncall/api/v0/teams.py index 7a86eaf..bf321e2 100755 --- a/src/oncall/api/v0/teams.py +++ b/src/oncall/api/v0/teams.py @@ -149,6 +149,9 @@ def on_post(req, resp): email = data.get('email') iris_plan = data.get('iris_plan') iris_enabled = data.get('iris_enabled', False) + override_number = data.get('override_phone_number') + if not override_number: + override_number = None # validate Iris plan if provided and Iris is configured if iris_plan is not None and iris.client is not None: @@ -160,8 +163,10 @@ def on_post(req, resp): cursor = connection.cursor() try: cursor.execute(''' - INSERT INTO `team` (`name`, `slack_channel`, `email`, `scheduling_timezone`, `iris_plan`, `iris_enabled`) - VALUES (%s, %s, %s, %s, %s, %s)''', (team_name, slack, email, scheduling_timezone, iris_plan, iris_enabled)) + INSERT INTO `team` (`name`, `slack_channel`, `email`, `scheduling_timezone`, `iris_plan`, `iris_enabled`, + `override_phone_number`) + VALUES (%s, %s, %s, %s, %s, %s, %s)''', + (team_name, slack, email, scheduling_timezone, iris_plan, iris_enabled, override_number)) team_id = cursor.lastrowid query = ''' diff --git a/src/oncall/ui/static/js/oncall.js b/src/oncall/ui/static/js/oncall.js index c31d825..a99f105 100644 --- a/src/oncall/ui/static/js/oncall.js +++ b/src/oncall/ui/static/js/oncall.js @@ -750,6 +750,7 @@ var oncall = { email = $form.find('#team-email').val(), slack = $form.find('#team-slack').val(), timezone = $form.find('#team-timezone').val(), + overrideNumber = $form.find('#team-override-phone').val(), irisPlan = $form.find('#team-irisplan').val(), irisEnabled = $form.find('#team-iris-enabled').prop('checked'), model = {}; @@ -775,6 +776,7 @@ var oncall = { email: email, slack_channel: slack, scheduling_timezone: timezone, + override_phone_number: overrideNumber, iris_plan: irisPlan, iris_enabled: irisEnabled ? '1' : '0', page: self.data.route @@ -2370,6 +2372,7 @@ var oncall = { $teamEmail = $modalForm.find('#team-email'), $teamSlack = $modalForm.find('#team-slack'), $teamTimezone = $modalForm.find('#team-timezone'), + $teamNumber = $modalForm.find('#team-override-phone'), $teamIrisPlan = $modalForm.find('#team-irisplan'), $teamIrisEnabled = $modalForm.find('#team-iris-enabled'), self = this, @@ -2383,6 +2386,7 @@ var oncall = { $teamName.val($btn.attr('data-modal-name')); $teamEmail.val($btn.attr('data-modal-email')); $teamSlack.val($btn.attr('data-modal-slack')); + $teamNumber.val($btn.attr('data-modal-override-phone')); $teamIrisPlan.val($btn.attr('data-modal-irisplan')); $teamIrisEnabled.prop('checked', $btn.attr('data-modal-iris-enabled') === '1'); $planInput = $('#team-irisplan'); diff --git a/src/oncall/ui/templates/base.html b/src/oncall/ui/templates/base.html index bc416ec..243c2f6 100644 --- a/src/oncall/ui/templates/base.html +++ b/src/oncall/ui/templates/base.html @@ -139,6 +139,9 @@ {% endfor %}
++ +
{% if iris_plan_settings.activated %}diff --git a/src/oncall/ui/templates/index.html b/src/oncall/ui/templates/index.html index 8c20de3..eafee41 100644 --- a/src/oncall/ui/templates/index.html +++ b/src/oncall/ui/templates/index.html @@ -277,7 +277,7 @@