1
0
mirror of https://github.com/linkedin/oncall.git synced 2025-11-27 23:18:38 +02:00

Add team subscriptions

This commit is contained in:
Daniel Wang
2017-11-03 10:57:51 -07:00
committed by Joe Gillotti
parent 7bb10a04d2
commit 2aee0c316c
12 changed files with 404 additions and 57 deletions

View File

@@ -417,6 +417,19 @@ CREATE TABLE IF NOT EXISTS `application` (
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `team_subscription` (
`team_id` BIGINT(20) UNSIGNED NOT NULL,
`subscription_id` BIGINT(20) UNSIGNED NOT NULL,
`role_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`team_id`, `subscription_id`, `role_id`),
INDEX `team_subscription_team_id_idx` (`team_id` ASC),
CONSTRAINT `team_subscription_team_id_fk` FOREIGN KEY (`team_id`) REFERENCES `team` (`id`)
ON DELETE CASCADE,
CONSTRAINT `team_subscription_subscription_id_fk` FOREIGN KEY (`subscription_id`) REFERENCES `team` (`id`)
ON DELETE CASCADE,
INDEX `team_subscription_team_id_fk_idx` (`team_id` ASC)
);
INSERT INTO `scheduler` ( `name`, `description`)
VALUES ('default',
'Default scheduling algorithm'),

View File

@@ -179,7 +179,7 @@ def event(team, role):
for ev in self.created:
requests.delete(api_v0('events/%d' % ev))
for t in self.teams:
re = requests.get(api_v0('events?team=' + t))
re = requests.get(api_v0('events?include_subscribed=false'), params={'team': t})
for ev in re.json():
requests.delete(api_v0('events/%d' % ev['id']))

View File

@@ -14,7 +14,7 @@ def test_invalid_events():
@prefix('test_events')
def test_events(team, user, role):
def test_events(event, team, user, role):
team_name = team.create()
team_name_2 = team.create()
user_name = user.create()
@@ -24,6 +24,8 @@ def test_events(team, user, role):
user.add_to_team(user_name, team_name)
user.add_to_team(user_name_2, team_name)
user.add_to_team(user_name_2, team_name_2)
event.teams.add(team_name)
event.teams.add(team_name_2)
start, end = int(time.time()) + 100, int(time.time() + 36000)

161
e2e/test_subscription.py Normal file
View File

@@ -0,0 +1,161 @@
import time
import requests
from testutils import prefix, api_v0
@prefix('test_v0_sub')
def test_api_v0_team_subscription(team, role):
team_name = team.create()
team_name_2 = team.create()
team_name_3 = team.create()
role_name = role.create()
re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_2})
assert re.status_code == 201
re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_3})
assert re.status_code == 201
re = requests.get(api_v0('teams/%s/subscriptions' % team_name))
assert re.status_code == 200
data = re.json()
assert team_name_2 in data
assert team_name_3 in data
assert len(data) == 2
re = requests.delete(api_v0('teams/%s/subscriptions/%s/%s' % (team_name, team_name_3, role_name)))
assert re.status_code == 200
re = requests.get(api_v0('teams/%s/subscriptions' % team_name))
assert re.status_code == 200
data = re.json()
assert team_name_2 in data
assert len(data) == 1
@prefix('test_v0_sub_events')
def test_api_v0_subscription_events(user, role, team, event):
team_name = team.create()
team_name_2 = team.create()
user_name = user.create()
role_name = role.create()
user.add_to_team(user_name, team_name)
user.add_to_team(user_name, team_name_2)
start = int(time.time()) + 1000
ev1 = event.create({'start': start,
'end': start + 1000,
'user': user_name,
'team': team_name,
'role': role_name})
ev2 = event.create({'start': start + 1000,
'end': start + 2000,
'user': user_name,
'team': team_name_2,
'role': role_name})
re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_2})
assert re.status_code == 201
re = requests.get(api_v0('events?team__eq=%s' % team_name))
ev_ids = [ev['id'] for ev in re.json()]
assert ev1 in ev_ids
assert ev2 in ev_ids
re = requests.get(api_v0('events?team__eq=%s&include_subscribed=False' % team_name))
ev_ids = [ev['id'] for ev in re.json()]
assert ev1 in ev_ids
assert len(ev_ids) == 1
@prefix('test_v0_sub_oncall')
def test_v0_subscription_oncall(user, role, team, service, event):
team_name = team.create()
team_name_2 = team.create()
service_name = service.create()
service.associate_team(service_name, team_name)
user_name = user.create()
user_name_2 = user.create()
role_name = role.create()
user.add_to_team(user_name, team_name)
user.add_to_team(user_name_2, team_name_2)
re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name, 'subscription': team_name_2})
assert re.status_code == 201
start = int(time.time())
ev1 = event.create({'start': start,
'end': start + 1000,
'user': user_name,
'team': team_name,
'role': role_name})
ev2 = event.create({'start': start,
'end': start + 1000,
'user': user_name_2,
'team': team_name_2,
'role': role_name})
re = requests.get(api_v0('services/%s/oncall/%s' % (service_name, role_name)))
assert re.status_code == 200
results = re.json()
users = [ev['user'] for ev in results]
assert user_name in users
assert user_name_2 in users
assert len(results) == 2
@prefix('test_v0_sub_summary')
def test_v0_subscription_summary(user, role, team, event):
team_name = team.create()
team_name_2 = team.create()
user_name = user.create()
user_name_2 = user.create()
role_name = role.create()
role_name_2 = role.create()
user.add_to_team(user_name, team_name)
user.add_to_team(user_name_2, team_name_2)
start, end = int(time.time()), int(time.time()+36000)
event_data_1 = {'start': start,
'end': end,
'user': user_name,
'team': team_name,
'role': role_name}
event_data_2 = {'start': start - 5,
'end': end - 5,
'user': user_name_2,
'team': team_name_2,
'role': role_name_2}
event_data_3 = {'start': start + 50000,
'end': end + 50000,
'user': user_name,
'team': team_name,
'role': role_name}
event_data_4 = {'start': start + 50005,
'end': end + 50005,
'user': user_name_2,
'team': team_name_2,
'role': role_name_2}
event_data_5 = {'start': start + 50001,
'end': end + 50001,
'user': user_name,
'team': team_name,
'role': role_name}
# Create current events
event.create(event_data_1)
event.create(event_data_2)
# Create next events
event.create(event_data_3)
event.create(event_data_4)
# Create extra future event that isn't the next event
event.create(event_data_5)
re = requests.post(api_v0('teams/%s/subscriptions' % team_name), json={'role': role_name_2, 'subscription': team_name_2})
assert re.status_code == 201
re = requests.get(api_v0('teams/%s/summary' % team_name))
assert re.status_code == 200
results = re.json()
keys = ['start', 'end', 'role']
assert all(results['current'][role_name][0][key] == event_data_1[key] for key in keys)
assert all(results['current'][role_name_2][0][key] == event_data_2[key] for key in keys)
assert all(results['next'][role_name][0][key] == event_data_3[key] for key in keys)
assert all(results['next'][role_name_2][0][key] == event_data_4[key] for key in keys)

View File

@@ -82,6 +82,10 @@ def init(application, config):
from . import timezones
application.add_route('/api/v0/timezones', timezones)
from . import team_subscription, team_subscriptions
application.add_route('/api/v0/teams/{team}/subscriptions', team_subscriptions)
application.add_route('/api/v0/teams/{team}/subscriptions/{subscription}/{role}', team_subscription)
# Optional Iris integration
from . import iris_settings
application.add_route('/api/v0/iris_settings', iris_settings)

View File

@@ -66,6 +66,8 @@ constraints = {
'user__endswith': '`user`.`name` LIKE CONCAT("%%", %s)'
}
TEAM_PARAMS = {'team', 'team__eq', 'team__contains', 'team__startswith', 'team_endswith', 'team_id'}
def on_get(req, resp):
"""
@@ -144,6 +146,10 @@ def on_get(req, resp):
"""
fields = req.get_param_as_list('fields', transform=columns.__getitem__)
req.params.pop('fields', None)
include_sub = req.get_param_as_bool('include_subscribed')
if include_sub is None:
include_sub = True
req.params.pop('include_subscribed', None)
cols = ', '.join(fields) if fields else all_columns
if any(key not in constraints for key in req.params):
raise HTTPBadRequest('Bad constraint param')
@@ -154,17 +160,42 @@ def on_get(req, resp):
where_params = []
where_vals = []
for key in req.params:
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
# Build where clause. If including subscriptions, deal with team parameters later
params = req.params.viewkeys() - TEAM_PARAMS if include_sub else req.params
for key in params:
val = req.get_param(key)
if key in constraints:
where_params.append(constraints[key])
where_vals.append(val)
# Deal with team subscriptions and team parameters
team_where = []
subs_vals = []
team_params = req.params.viewkeys() & TEAM_PARAMS
if include_sub and team_params:
for key in team_params:
val = req.get_param(key)
team_where.append(constraints[key])
subs_vals.append(val)
subs_and = ' AND '.join(team_where)
cursor.execute('''SELECT `subscription_id`, `role_id` FROM `team_subscription`
JOIN `team` ON `team_id` = `team`.`id`
WHERE %s''' % subs_and,
subs_vals)
if cursor.rowcount != 0:
# Check conditions are true for either team OR subscriber
subs_and = '(%s OR (%s))' % (subs_and, ' OR '.join(['`team`.`id` = %s AND `role`.`id` = %s' %
(row['subscription_id'], row['role_id']) for row in cursor]))
where_params.append(subs_and)
where_vals += subs_vals
where_query = ' AND '.join(where_params)
if where_query:
query = '%s WHERE %s' % (query, where_query)
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
cursor.execute(query, where_vals)
data = cursor.fetchall()
cursor.close()

View File

@@ -40,24 +40,43 @@ def on_get(req, resp, service, role=None):
]
'''
get_oncall_query = '''SELECT `user`.`full_name` AS `full_name`, `event`.`start`, `event`.`end`,
`contact_mode`.`name` AS `mode`, `user_contact`.`destination`, `role`.`name` AS `role`,
`team`.`name` AS `team`, `user`.`name` AS `user`
FROM `service` JOIN `team_service` ON `service`.`id` = `team_service`.`service_id`
JOIN `event` ON `event`.`team_id` = `team_service`.`team_id`
JOIN `user` ON `user`.`id` = `event`.`user_id`
JOIN `role` ON `role`.`id` = `event`.`role_id`
JOIN `team` ON `team`.`id` = `event`.`team_id`
LEFT JOIN `user_contact` ON `user`.`id` = `user_contact`.`user_id`
LEFT JOIN `contact_mode` ON `contact_mode`.`id` = `user_contact`.`mode_id`
WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end`
AND `service`.`name` = %s '''
query_params = [service]
get_oncall_query = '''
SELECT `user`.`full_name` AS `full_name`,
`event`.`start`, `event`.`end`,
`contact_mode`.`name` AS `mode`,
`user_contact`.`destination`,
`user`.`name` AS `user`,
`team`.`name` AS `team`,
`role`.`name` AS `role`
FROM `event`
JOIN `user` ON `event`.`user_id` = `user`.`id`
JOIN `team` ON `event`.`team_id` = `team`.`id`
JOIN `role` ON `role`.`id` = `event`.`role_id`
LEFT JOIN `team_subscription` ON `subscription_id` = `team`.`id`
AND `team_subscription`.`role_id` = `role`.`id`
LEFT JOIN `user_contact` ON `user`.`id` = `user_contact`.`user_id`
LEFT JOIN `contact_mode` ON `contact_mode`.`id` = `user_contact`.`mode_id`
WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end`
AND (`team`.`id` IN %s OR `team_subscription`.`team_id` IN %s)'''
query_params = []
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`
JOIN `service` ON `service`.`id` = `team_service`.`service_id`
WHERE `service`.`name` = %s''',
service)
team_ids = [row['team_id'] for row in cursor]
if not team_ids:
resp.body = json_dumps([])
cursor.close()
connection.close()
return
query_params += [team_ids, team_ids]
if role is not None:
get_oncall_query += ' AND `role`.`name` = %s'
query_params.append(role)
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
cursor.execute(get_oncall_query, query_params)
data = cursor.fetchall()
ret = {}

View File

@@ -64,15 +64,18 @@ def on_get(req, resp, team, role=None):
JOIN `user` ON `event`.`user_id` = `user`.`id`
JOIN `team` ON `event`.`team_id` = `team`.`id`
JOIN `role` ON `role`.`id` = `event`.`role_id`
LEFT JOIN `team_subscription` ON `subscription_id` = `team`.`id`
AND `team_subscription`.`role_id` = `role`.`id`
LEFT JOIN `team` `subscriber` ON `subscriber`.`id` = `team_subscription`.`team_id`
LEFT JOIN `user_contact` ON `user`.`id` = `user_contact`.`user_id`
LEFT JOIN `contact_mode` ON `contact_mode`.`id` = `user_contact`.`mode_id`
WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end`
AND `team`.`name` = %s'''
query_params = [team]
AND (`team`.`name` = %s OR `subscriber`.`name` = %s)'''
query_params = [team, team]
if role is not None:
get_oncall_query += 'AND `role`.`name` = %s'
get_oncall_query += ' AND `role`.`name` = %s'
query_params.append(role)
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
cursor.execute(get_oncall_query, query_params)

View File

@@ -0,0 +1,22 @@
from ... import db
from ...auth import login_required, check_team_auth
from falcon import HTTPNotFound
@login_required
def on_delete(req, resp, team, subscription, role):
check_team_auth(team, req)
connection = db.connect()
cursor = connection.cursor()
cursor.execute('''DELETE FROM `team_subscription`
WHERE team_id = (SELECT `id` FROM `team` WHERE `name` = %s)
AND `subscription_id` = (SELECT `id` FROM `team` WHERE `name` = %s)\
AND `role_id` = (SELECT `id` FROM `role` WHERE `name` = %s)''',
(team, subscription, role))
deleted = cursor.rowcount
connection.commit()
cursor.close()
connection.close()
if deleted == 0:
raise HTTPNotFound()

View File

@@ -0,0 +1,58 @@
from ... import db
from ujson import dumps as json_dumps
from falcon import HTTPError, HTTPBadRequest, HTTP_201
from ...utils import load_json_body
from ...auth import login_required, check_team_auth
import logging
logger = logging.getLogger('oncall-api')
def on_get(req, resp, team):
connection = db.connect()
cursor = connection.cursor()
cursor.execute('''SELECT `subscription`.`name`, `role`.`name` FROM `team`
JOIN `team_subscription` ON `team`.`id` = `team_subscription`.`team_id`
JOIN `team` `subscription` ON `subscription`.`id` = `team_subscription`.`subscription_id`
JOIN `role` ON `role`.`id` = `team_subscription`.`role_id`
WHERE `team`.`name` = %s''',
team)
data = [row[0] for row in cursor]
cursor.close()
connection.close()
resp.body = json_dumps(data)
@login_required
def on_post(req, resp, team):
data = load_json_body(req)
check_team_auth(team, req)
sub_name = data.get('subscription')
role_name = data.get('role')
if not sub_name or not role_name:
raise HTTPBadRequest('Invalid subscription', 'Missing subscription name or role name')
connection = db.connect()
cursor = connection.cursor()
try:
cursor.execute('''INSERT INTO `team_subscription` (`team_id`, `subscription_id`, `role_id`) VALUES
((SELECT `id` FROM `team` WHERE `name` = %s),
(SELECT `id` FROM `team` WHERE `name` = %s),
(SELECT `id` FROM `role` WHERE `name` = %s))''',
(team, sub_name, role_name))
except db.IntegrityError as e:
err_msg = str(e.args[1])
if err_msg == 'Column \'team_id\' cannot be null':
err_msg = 'team "%s" not found' % team
elif err_msg == 'Column \'role_id\' cannot be null':
err_msg = 'role "%s" not found' % role_name
elif err_msg == 'Column \'subscription_id\' cannot be null':
err_msg = 'team "%s" not found' % sub_name
logger.exception('Unknown integrity error in team_subscriptions')
raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
else:
connection.commit()
finally:
cursor.close()
connection.close()
resp.status = HTTP_201

View File

@@ -114,17 +114,32 @@ def on_get(req, resp, team):
if cursor.rowcount < 1:
raise HTTPNotFound()
team_id = cursor.fetchone()['id']
current_query = '''
SELECT `role`.`name` AS `role`, `user`.`full_name` AS `full_name`,
`event`.`start`, `event`.`end`, `user`.`photo_url`, `event`.`user_id`
SELECT `user`.`full_name` AS `full_name`,
`user`.`photo_url`,
`event`.`start`, `event`.`end`,
`event`.`user_id`,
`user`.`name` AS `user`,
`team`.`name` AS `team`,
`role`.`name` AS `role`
FROM `event`
JOIN `role` ON `event`.`role_id` = `role`.`id`
JOIN `user` ON `event`.`user_id` = `user`.`id`
WHERE `event`.`team_id` = %s
AND UNIX_TIMESTAMP() >= `event`.`start`
AND UNIX_TIMESTAMP() < `event`.`end`'''
cursor.execute(current_query, team_id)
JOIN `user` ON `event`.`user_id` = `user`.`id`
JOIN `team` ON `event`.`team_id` = `team`.`id`
JOIN `role` ON `role`.`id` = `event`.`role_id`
WHERE UNIX_TIMESTAMP() BETWEEN `event`.`start` AND `event`.`end`'''
team_where = '`team`.`id` = %s'
cursor.execute('''SELECT `subscription_id`, `role_id` FROM `team_subscription`
JOIN `team` ON `team_id` = `team`.`id`
WHERE %s''' % team_where,
team_id)
if cursor.rowcount != 0:
# Check conditions are true for either team OR subscriber
team_where = '(%s OR (%s))' % (team_where, ' OR '.join(['`event`.`team_id` = %s AND `event`.`role_id` = %s' %
(row['subscription_id'], row['role_id']) for row in cursor]))
cursor.execute(' AND '.join((current_query, team_where)), team_id)
payload = {}
users = set([])
payload['current'] = defaultdict(list)
@@ -133,18 +148,25 @@ def on_get(req, resp, team):
users.add(event['user_id'])
next_query = '''
SELECT `role`.`name` AS `role`, `user`.`full_name` AS `full_name`,
`event`.`start`, `event`.`end`, `user`.`photo_url`, `event`.`user_id`
SELECT `role`.`name` AS `role`,
`user`.`full_name` AS `full_name`,
`event`.`start`,
`event`.`end`,
`user`.`photo_url`,
`event`.`user_id`,
`event`.`role_id`,
`event`.`team_id`
FROM `event`
JOIN `role` ON `event`.`role_id` = `role`.`id`
JOIN `user` ON `event`.`user_id` = `user`.`id`
JOIN (SELECT `role_id`, `team_id`, MIN(`start` - UNIX_TIMESTAMP()) AS dist
FROM `event`
WHERE `start` > UNIX_TIMESTAMP() AND `event`.`team_id` = %s
GROUP BY role_id) AS t1
ON `event`.`role_id` = `t1`.`role_id`
AND `event`.`start` - UNIX_TIMESTAMP() = `t1`.dist
AND `event`.`team_id` = `t1`.`team_id`'''
JOIN `role` ON `event`.`role_id` = `role`.`id`
JOIN `user` ON `event`.`user_id` = `user`.`id`
JOIN (SELECT `event`.`role_id`, `event`.`team_id`, MIN(`event`.`start` - UNIX_TIMESTAMP()) AS dist
FROM `event` JOIN `team` ON `team`.`id` = `event`.`team_id`
WHERE `start` > UNIX_TIMESTAMP() AND %s
GROUP BY `event`.`role_id`, `event`.`team_id`) AS t1
ON `event`.`role_id` = `t1`.`role_id`
AND `event`.`start` - UNIX_TIMESTAMP() = `t1`.dist
AND `event`.`team_id` = `t1`.`team_id`''' % team_where
cursor.execute(next_query, team_id)
payload['next'] = defaultdict(list)
for event in cursor:

View File

@@ -4,6 +4,8 @@
from ... import db
from .events import all_columns
from ujson import dumps as json_dumps
from collections import defaultdict
import operator
def on_get(req, resp, user_name):
@@ -47,29 +49,39 @@ def on_get(req, resp, user_name):
'''
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
role = req.get_param('role', None)
limit = req.get_param_as_int('limit', None)
limit = req.get_param_as_int('limit')
query_end = ' ORDER BY `event`.`start` ASC'
query = '''SELECT %s, (SELECT COUNT(*) FROM `event` `counter`
WHERE `counter`.`link_id` = `event`.`link_id`) AS num_events
query = '''SELECT %s
FROM `event`
JOIN `user` ON `user`.`id` = `event`.`user_id`
JOIN `team` ON `team`.`id` = `event`.`team_id`
JOIN `role` ON `role`.`id` = `event`.`role_id`
LEFT JOIN `event` `e2` ON `event`.link_id = `e2`.`link_id` AND `e2`.`start` < `event`.`start`
WHERE `user`.`id` = (SELECT `id` FROM `user` WHERE `name` = %%s)
AND `event`.`start` > UNIX_TIMESTAMP()
AND `e2`.`start` IS NULL''' % all_columns
AND `event`.`start` > UNIX_TIMESTAMP()''' % all_columns
query_params = [user_name]
if role:
query_end = ' AND `role`.`name` = %s' + query_end
query_params.append(role)
if limit:
query_end += ' LIMIT %s'
query_params.append(limit)
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
cursor.execute(query + query_end, query_params)
data = cursor.fetchall()
resp.body = json_dumps(data)
cursor.close()
connection.close()
links = defaultdict(list)
formatted = []
for event in data:
if event['link_id'] is None:
formatted.append(event)
else:
links[event['link_id']].append(event)
for events in links.itervalues():
first_event = min(events, key=operator.itemgetter('start'))
first_event['num_events'] = len(events)
formatted.append(first_event)
formatted = sorted(formatted, key=operator.itemgetter('start'))
if limit is not None:
formatted = formatted[:limit]
resp.body = json_dumps(formatted)