You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-26 23:10:47 +02:00
Add UI page for team subscriptions
Also, get user data if not available in calendar events
This commit is contained in:
@@ -18,8 +18,8 @@ def test_api_v0_team_subscription(team, role):
|
||||
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 {'role': role_name, 'subscription': team_name_2} in data
|
||||
assert {'role': role_name, 'subscription': 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)))
|
||||
@@ -28,7 +28,7 @@ def test_api_v0_team_subscription(team, role):
|
||||
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 {'role': role_name, 'subscription': team_name_2} in data
|
||||
assert len(data) == 1
|
||||
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ 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`
|
||||
cursor = connection.cursor(db.DictCursor)
|
||||
cursor.execute('''SELECT `subscription`.`name` AS `subscription`, `role`.`name` AS `role` 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]
|
||||
data = [row for row in cursor]
|
||||
cursor.close()
|
||||
connection.close()
|
||||
resp.body = json_dumps(data)
|
||||
@@ -31,6 +31,8 @@ def on_post(req, resp, team):
|
||||
role_name = data.get('role')
|
||||
if not sub_name or not role_name:
|
||||
raise HTTPBadRequest('Invalid subscription', 'Missing subscription name or role name')
|
||||
if sub_name == team:
|
||||
raise HTTPBadRequest('Invalid subscription', 'Subscription team must be different from subscribing team')
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
@@ -42,12 +44,15 @@ def on_post(req, resp, team):
|
||||
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
|
||||
err_msg = 'Team "%s" not found' % team
|
||||
elif err_msg == 'Column \'role_id\' cannot be null':
|
||||
err_msg = 'role "%s" not found' % role_name
|
||||
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')
|
||||
err_msg = 'Team "%s" not found' % sub_name
|
||||
elif err_msg.startswith('Duplicate entry'):
|
||||
err_msg = 'Subscription already exists'
|
||||
else:
|
||||
logger.exception('Unknown integrity error in team_subscriptions')
|
||||
raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
|
||||
else:
|
||||
connection.commit()
|
||||
|
||||
@@ -1237,6 +1237,75 @@ overflow: hidden;
|
||||
min-width: 95px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Team subscription page
|
||||
*/
|
||||
|
||||
.subscription-actions {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.module-subscription {
|
||||
width: 24%;
|
||||
margin: 0 .5%;
|
||||
float: left;
|
||||
transition: box-shadow .15s;
|
||||
}
|
||||
|
||||
.module-subscription .subscription-actions {
|
||||
border-top: 5px solid #FDE3D2;
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.module-subscription .subscription-actions > span {
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
opacity: .3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.module-subscription .subscription-actions > span .grey-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.module-subscription .subscription-actions > span:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.module-subscription .label-col {
|
||||
width: 27%;
|
||||
max-width: 110px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.module-subscription .data-col {
|
||||
text-transform: capitalize;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.module-subscription .data-col.subscription-role {
|
||||
display: inline-block;
|
||||
width: 167px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.module-subscription .subscription-actions {
|
||||
border-top: 5px solid #CCC;
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.module-subscription-create .form-control,
|
||||
#subscription-team-container {
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* User notifications page
|
||||
*/
|
||||
|
||||
@@ -225,6 +225,12 @@ var oncall = {
|
||||
self.team.init(params.name, 'schedules');
|
||||
self.team.schedules.init(params.name);
|
||||
},
|
||||
'team/:name/subscriptions': function(params){
|
||||
oncall.callbacks.onLogin = $.noop;
|
||||
oncall.callbacks.onLogout = $.noop;
|
||||
self.team.init(params.name, 'subscriptions');
|
||||
self.team.subscriptions.init(params.name);
|
||||
},
|
||||
'team/:name': function(params){
|
||||
oncall.callbacks.onLogin = $.noop;
|
||||
oncall.callbacks.onLogout = $.noop;
|
||||
@@ -850,6 +856,7 @@ var oncall = {
|
||||
calendar: '#calendar-container',
|
||||
$calendar: null,
|
||||
url: '/api/v0/teams/',
|
||||
userUrl: '/api/v0/users/',
|
||||
pageSource: $('#team-calendar-template').html(),
|
||||
escalateModalTemplate: $('#team-escalate-modal'),
|
||||
cardColumnTemplate: $('#card-column-template').html(),
|
||||
@@ -1072,36 +1079,62 @@ var oncall = {
|
||||
// #TODO: Leverage this to create whole modal
|
||||
var $ul = $modal.find('.inc-event-details-view'),
|
||||
$title = $modal.find('.inc-event-details-title'),
|
||||
userData = this.data.teamData.users[evt.user];
|
||||
userData = this.data.teamData.users[evt.user],
|
||||
userPromise = $.Deferred(),
|
||||
self = this;
|
||||
|
||||
$title.text(userData.full_name);
|
||||
$ul
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">E-Mail</label>')
|
||||
.append('<span class="data-col"><a href="mailto:' + userData.contacts.email + '" target="_blank">' + userData.contacts.email + '</a></span>')
|
||||
)
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">Call</label>')
|
||||
.append('<span class="data-col"><a href="tel:' + userData.contacts.call + '">' + userData.contacts.call + '</a></span>')
|
||||
)
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">SMS</label>')
|
||||
.append('<span class="data-col"><a href="tel:' + userData.contacts.sms + '">' + userData.contacts.sms + '</a></span>')
|
||||
)
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">Slack</label>')
|
||||
.append('<span class="data-col">' + userData.contacts.slack + '</span>')
|
||||
);
|
||||
if (evt.schedule_id) {
|
||||
$ul.append(
|
||||
$('<li />')
|
||||
.append('<small>This event is auto generated by the scheduler</small>')
|
||||
);
|
||||
if (userData !== undefined) {
|
||||
userPromise.resolve();
|
||||
} else {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: this.data.userUrl + evt.user,
|
||||
contentType: 'application/json',
|
||||
dataType: 'html'
|
||||
}).done(function(response){
|
||||
userData = JSON.parse(response);
|
||||
self.data.teamData.users[evt.user] = userData;
|
||||
userPromise.resolve();
|
||||
}).fail(function(data){
|
||||
userPromise.reject();
|
||||
})
|
||||
}
|
||||
userPromise.done(function() {
|
||||
$title.text(userData.full_name);
|
||||
$ul
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">E-Mail</label>')
|
||||
.append('<span class="data-col"><a href="mailto:' + userData.contacts.email + '" target="_blank">' + userData.contacts.email + '</a></span>')
|
||||
)
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">Call</label>')
|
||||
.append('<span class="data-col"><a href="tel:' + userData.contacts.call + '">' + userData.contacts.call + '</a></span>')
|
||||
)
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">SMS</label>')
|
||||
.append('<span class="data-col"><a href="tel:' + userData.contacts.sms + '">' + userData.contacts.sms + '</a></span>')
|
||||
)
|
||||
.append(
|
||||
$('<li />')
|
||||
.append('<label class="label-col">Slack</label>')
|
||||
.append('<span class="data-col">' + userData.contacts.slack + '</span>')
|
||||
);
|
||||
if (evt.schedule_id) {
|
||||
$ul.append(
|
||||
$('<li />')
|
||||
.append('<small>This event is auto generated by the scheduler</small>')
|
||||
);
|
||||
}
|
||||
if (evt.team !== self.data.teamName) {
|
||||
$ul.append(
|
||||
$('<li />')
|
||||
.append('<small>This is a subscription event from ' + evt.team + '</small>')
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
updatePlanDescription: function() {
|
||||
var $modal = $(this.data.escalateModal),
|
||||
@@ -1469,6 +1502,138 @@ var oncall = {
|
||||
});
|
||||
}
|
||||
},
|
||||
subscriptions: {
|
||||
data: {
|
||||
$page: $('.content-wrapper'),
|
||||
url: '/api/v0/teams/',
|
||||
subscriptionUrl: '/subscriptions',
|
||||
teamName: null,
|
||||
pageSource: $('#team-subscriptions-template').html(),
|
||||
moduleSubscriptionTemplate: $('#module-subscription-template').html(),
|
||||
moduleSubscriptionCreateTemplate: $('#module-subscription-create-template').html(),
|
||||
addSubscriptionItem: '#add-subscription',
|
||||
saveSubscription: '#save-subscription',
|
||||
addSubscriptionContainer: '.add-subscription-container',
|
||||
subscriptionCreateForm: '.module-subscription-create',
|
||||
moduleSubscriptionsWrapper: '.module-subscriptions-wrapper',
|
||||
deleteSubscriptionCard: '.delete-subscription-item',
|
||||
subscriptionItem: '.module-subscription',
|
||||
subscriptionCount: '.subscription-count'
|
||||
},
|
||||
init: function(name){
|
||||
Handlebars.registerPartial('module-subscription', this.data.moduleSubscriptionTemplate);
|
||||
Handlebars.registerPartial('module-subscription-create', this.data.moduleSubscriptionCreateTemplate);
|
||||
this.data.teamName = decodeURIComponent(name);
|
||||
this.getData();
|
||||
},
|
||||
events: function(){
|
||||
router.updatePageLinks();
|
||||
this.data.$page.on('click', this.data.addSubscriptionItem, this.addSubscriptionItem.bind(this));
|
||||
this.data.$page.on('submit', this.data.subscriptionCreateForm, this.saveSubscription.bind(this));
|
||||
this.data.$page.on('click', this.data.deleteSubscriptionCard, this.deleteSubscription.bind(this));
|
||||
},
|
||||
getData: function(){
|
||||
var template = Handlebars.compile(this.data.pageSource),
|
||||
self = this;
|
||||
|
||||
$.when($.getJSON(this.data.url + this.data.teamName + this.data.subscriptionUrl),
|
||||
$.getJSON(this.data.url + this.data.teamName),
|
||||
oncall.data.rolesPromise).done(function(subData, teamData){
|
||||
data = teamData[0];
|
||||
data.subscriptions = subData[0];
|
||||
data.roles = oncall.data.roles;
|
||||
|
||||
self.data.teamData = data;
|
||||
self.data.$page.html(template(data));
|
||||
self.events();
|
||||
self.renderSubscriptionCounts();
|
||||
}).fail(function(error){
|
||||
var data = {
|
||||
error: true,
|
||||
error_code: error.status,
|
||||
error_status: error.statusText,
|
||||
error_text: name + ' team not found'
|
||||
};
|
||||
self.data.$page.html(template(data));
|
||||
});
|
||||
},
|
||||
addSubscriptionItem: function(e){
|
||||
var template = Handlebars.compile(this.data.moduleSubscriptionCreateTemplate),
|
||||
$container = $(e.target).parents().find(this.data.addSubscriptionContainer),
|
||||
teamData = $.extend(true, {}, this.data.teamData);
|
||||
$container.prepend(template(teamData));
|
||||
oncall.typeahead.init();
|
||||
},
|
||||
saveSubscription: function(e){
|
||||
e.preventDefault();
|
||||
|
||||
var self = this,
|
||||
$form = $(e.target),
|
||||
subscription = {},
|
||||
$cta = $form.find(this.data.saveSubscription),
|
||||
template = Handlebars.compile(this.data.moduleSubscriptionTemplate),
|
||||
url = this.data.url + this.data.teamName + this.data.subscriptionUrl,
|
||||
method = 'POST';
|
||||
|
||||
subscription.subscription = $form.find('input.typeahead.tt-input.subscription-team').val();
|
||||
subscription.role = $form.find('.subscription-role').val();
|
||||
if (subscription.role === undefined || subscription.subscription === '') {
|
||||
oncall.alerts.createAlert('Invalid or missing field.');
|
||||
} else {
|
||||
oncall.alerts.removeAlerts();
|
||||
$.ajax({
|
||||
type: method,
|
||||
url: url,
|
||||
contentType: 'application/json',
|
||||
dataType: 'html',
|
||||
data: JSON.stringify(subscription)
|
||||
}).done(function () {
|
||||
self.data.$page.find(self.data.moduleSubscriptionsWrapper).append(template(subscription));
|
||||
$form.remove();
|
||||
self.renderSubscriptionCounts();
|
||||
}).fail(function (data) {
|
||||
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Request failed.';
|
||||
oncall.alerts.createAlert(error, 'danger');
|
||||
}).always(function () {
|
||||
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteSubscriptionItem: function($modal, $caller){
|
||||
var $form = $caller.parents(this.data.subscriptionCreateForm);
|
||||
$form.remove();
|
||||
$modal.modal('hide');
|
||||
},
|
||||
deleteSubscription: function($modal, $caller) {
|
||||
var $card = $caller.parents('.module-card'),
|
||||
$modalBody = $modal.find('.modal-body'),
|
||||
$cta = $modal.find('.modal-cta'),
|
||||
role = $card.attr('data-role'),
|
||||
subscription = $card.attr('data-team'),
|
||||
url = this.data.url + this.data.teamName + '/subscriptions/' + subscription + '/' + role,
|
||||
self = this;
|
||||
|
||||
$cta.addClass('loading disabled').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: url,
|
||||
dataType: 'html'
|
||||
}).done(function(){
|
||||
$modal.modal('hide');
|
||||
$card.remove();
|
||||
self.renderSubscriptionCounts();
|
||||
}).fail(function(data){
|
||||
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Delete failed.';
|
||||
oncall.alerts.createAlert(error, 'danger', $modalBody);
|
||||
}).always(function(){
|
||||
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||
});
|
||||
},
|
||||
renderSubscriptionCounts: function() {
|
||||
$(this.data.subscriptionCount).text($(this.data.moduleSubscriptionsWrapper).find(this.data.subscriptionItem).length);
|
||||
}
|
||||
},
|
||||
schedules: {
|
||||
data: {
|
||||
$page: $('.content-wrapper'),
|
||||
@@ -2671,7 +2836,7 @@ var oncall = {
|
||||
type = $this.attr('data-type') || 'users',
|
||||
results;
|
||||
|
||||
if (type === 'services') {
|
||||
if (type === 'services' || type == 'teams') {
|
||||
results = new Bloodhound({
|
||||
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
|
||||
@@ -322,6 +322,7 @@
|
||||
<li {{#isEqual page "calendar"}}class="active"{{/isEqual}} href="/team/{{name}}" data-navigo> Calendar </li>
|
||||
<li {{#isEqual page "info"}}class="active"{{/isEqual}} href="/team/{{name}}/info" data-navigo> Team Info </li>
|
||||
<li data-admin-action="true" {{#isEqual page "schedules"}}class="active"{{/isEqual}} href="/team/{{name}}/schedules" data-navigo> Schedule Templates </li>
|
||||
<li data-admin-action="true" {{#isEqual page "subscriptions"}}class="active"{{/isEqual}} href="/team/{{name}}/subscriptions" data-navigo> Subscriptions </li>
|
||||
{{#isEqual page "calendar"}}
|
||||
<span class="timezone-display-container pull-right">
|
||||
Display Timezone:
|
||||
@@ -603,6 +604,89 @@
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!--// **********************
|
||||
Team.subscriptions Page
|
||||
*********************** //-->
|
||||
<script id="team-subscriptions-template" type="text/x-handlebars-template">
|
||||
{{#if error}}
|
||||
{{>error-page this}}
|
||||
{{else}}
|
||||
<div class="subheader-wrapper">
|
||||
{{>team-subheader name=name page="subscriptions"}}
|
||||
</div>
|
||||
<div class="main container-fluid">
|
||||
<h3 class="module-heading"> Subscriptions <button id="add-subscription" class="btn btn-primary pull-right">+ New Subscription</button></h3>
|
||||
<div class="add-subscription-container">
|
||||
<!--// create form renders here -->
|
||||
</div>
|
||||
<div class="module module-subscriptions-container">
|
||||
<h4 class="module-heading border-bottom"> You have <span class="subscription-count"> 0 </span> subscriptions </h4>
|
||||
<div class="module-subscriptions-wrapper clearfix" data-type="subscription">
|
||||
{{#each this.subscriptions}}
|
||||
{{>module-subscription viewType="schedule-item"}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</script>
|
||||
|
||||
<script id="module-subscription-template" type="text/x-handlebars-template">
|
||||
<div class="module module-card module-subscription" data-role="{{role}}" data-team="{{subscription}}">
|
||||
<ul class="subscription-details">
|
||||
<li>
|
||||
<label class="light label-col">Team:</label>
|
||||
<span class="data-col">{{subscription}}</span>
|
||||
</li>
|
||||
<li>
|
||||
<label class="light label-col">Role:</label>
|
||||
<span class="data-col">{{role}} </span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="subscription-actions border-top">
|
||||
<span class="delete-{{viewType}}" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.subscriptions.deleteSubscription" data-modal-title="Delete subscription" data-modal-content="Delete subscription to {{role}} events from {{subscription}}?">
|
||||
<i class="svg-icon svg-icon-trash grey-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 8 8">
|
||||
<path d="M3 0c-.55 0-1 .45-1 1h-1c-.55 0-1 .45-1 1h7c0-.55-.45-1-1-1h-1c0-.55-.45-1-1-1h-1zm-2 3v4.81c0 .11.08.19.19.19h4.63c.11 0 .19-.08.19-.19v-4.81h-1v3.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-3.5h-1v3.5c0 .28-.22.5-.5.5s-.5-.22-.5-.5v-3.5h-1z" />
|
||||
</svg>
|
||||
</i>
|
||||
Delete
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- subscription create partial -->
|
||||
<script id="module-subscription-create-template" type="text/x-handlebars-template">
|
||||
<form class="module module-card module-subscription-create" >
|
||||
<div class="module-heading module-card-heading border-bottom">
|
||||
<h4>
|
||||
Create a new subscription
|
||||
</h4>
|
||||
</div>
|
||||
<div class="subscription-create-body">
|
||||
Add
|
||||
<select name="from" class="form-control subscription-role">
|
||||
{{#each roles}}
|
||||
<option value="{{this.name}}" {{isSelected ../selected_subscription.role this.name}}>
|
||||
{{this.name}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
events from the
|
||||
<span id="subscription-team-container">
|
||||
<input type="text" class="form-control typeahead subscription-team" placeholder="Team" data-type="teams" />
|
||||
</span>
|
||||
calendar to this team's calendar.
|
||||
<div class="border-top subscription-actions clearfix">
|
||||
<div class="pull-right">
|
||||
<button id="save-subscription" class="btn btn-primary" type="submit"><span class="btn-text">Save</span> <i class="loader loader-small"></i> </button>
|
||||
<button class="btn btn-blue delete-subscription" type="button" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.team.subscriptions.deleteSubscriptionItem" data-modal-title="Your changes are unsaved" data-modal-content="Aborting now will lose unsaved changes."> Cancel </button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<!--// **********************
|
||||
Team.schedules Page
|
||||
*********************** //-->
|
||||
|
||||
Reference in New Issue
Block a user