From 29ad8e3d0279bb11561053c20ac713521c899953 Mon Sep 17 00:00:00 2001
From: Daniel Wang
Date: Wed, 3 Jan 2018 17:17:49 -0800
Subject: [PATCH] Add override phone number to teams
Allows teams to configure a phone number that overrides a user's
regular number when they're the current primary on-call. This
lets teams pass around an on-call pager with a single phone
number, for example.
---
db/dummy_data.sql | 2 +-
db/schema.v0.sql | 1 +
e2e/conftest.py | 4 +--
e2e/test_services.py | 35 +++++++++++++++++++++
e2e/test_teams.py | 48 +++++++++++++++++++++++++++--
src/oncall/api/v0/service_oncall.py | 12 ++++++--
src/oncall/api/v0/team.py | 7 +++--
src/oncall/api/v0/team_oncall.py | 7 +++++
src/oncall/api/v0/team_summary.py | 14 +++++++--
src/oncall/api/v0/teams.py | 9 ++++--
src/oncall/ui/static/js/oncall.js | 4 +++
src/oncall/ui/templates/base.html | 3 ++
src/oncall/ui/templates/index.html | 2 +-
13 files changed, 132 insertions(+), 16 deletions(-)
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 @@
{{name}}
-
+