You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-29 23:38:17 +02:00
@@ -148,6 +148,13 @@ index_content_setting:
|
|||||||
missing_number_note: 'No number'
|
missing_number_note: 'No number'
|
||||||
header_color: '#3a3a3a'
|
header_color: '#3a3a3a'
|
||||||
|
|
||||||
|
# The base url for the public oncall calendar. This url has to open to the public internet for most web calendar subscriptions to work.
|
||||||
|
# The public calendar url will be formatted as follows: "{public_calendar_base_url}/{ical_key}".
|
||||||
|
# Replace localhost with the hostname of the oncall or iris-relay instance.
|
||||||
|
public_calendar_base_url: 'http://localhost:8080/api/v0/ical'
|
||||||
|
# Additional message you want to put here, could be a link to the FAQ
|
||||||
|
public_calendar_additional_message: 'Link to FAQ'
|
||||||
|
|
||||||
# Integration with Iris, allowing for escalation from Oncall
|
# Integration with Iris, allowing for escalation from Oncall
|
||||||
iris_plan_integration:
|
iris_plan_integration:
|
||||||
activated: True
|
activated: True
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ def index(req, resp):
|
|||||||
header_color=HEADER_COLOR,
|
header_color=HEADER_COLOR,
|
||||||
iris_plan_settings=IRIS_PLAN_SETTINGS,
|
iris_plan_settings=IRIS_PLAN_SETTINGS,
|
||||||
usercontact_ui_readonly=USERCONTACT_UI_READONLY,
|
usercontact_ui_readonly=USERCONTACT_UI_READONLY,
|
||||||
|
public_calendar_base_url=PUBLIC_CALENDAR_BASE_URL,
|
||||||
|
public_calendar_additional_message=PUBLIC_CALENDAR_ADDITIONAL_MESSAGE,
|
||||||
footer=INDEX_CONTENT_SETTING['footer'],
|
footer=INDEX_CONTENT_SETTING['footer'],
|
||||||
timezones=SUPPORTED_TIMEZONES
|
timezones=SUPPORTED_TIMEZONES
|
||||||
)
|
)
|
||||||
@@ -134,11 +136,15 @@ def init(application, config):
|
|||||||
global HEADER_COLOR
|
global HEADER_COLOR
|
||||||
global IRIS_PLAN_SETTINGS
|
global IRIS_PLAN_SETTINGS
|
||||||
global USERCONTACT_UI_READONLY
|
global USERCONTACT_UI_READONLY
|
||||||
|
global PUBLIC_CALENDAR_BASE_URL
|
||||||
|
global PUBLIC_CALENDAR_ADDITIONAL_MESSAGE
|
||||||
global LOGIN_REQUIRED
|
global LOGIN_REQUIRED
|
||||||
SLACK_INSTANCE = config.get('slack_instance')
|
SLACK_INSTANCE = config.get('slack_instance')
|
||||||
HEADER_COLOR = config.get('header_color', '#3a3a3a')
|
HEADER_COLOR = config.get('header_color', '#3a3a3a')
|
||||||
IRIS_PLAN_SETTINGS = config.get('iris_plan_integration')
|
IRIS_PLAN_SETTINGS = config.get('iris_plan_integration')
|
||||||
USERCONTACT_UI_READONLY = config.get('usercontact_ui_readonly', True)
|
USERCONTACT_UI_READONLY = config.get('usercontact_ui_readonly', True)
|
||||||
|
PUBLIC_CALENDAR_BASE_URL = config.get('public_calendar_base_url')
|
||||||
|
PUBLIC_CALENDAR_ADDITIONAL_MESSAGE = config.get('public_calendar_additional_message')
|
||||||
LOGIN_REQUIRED = config.get('require_auth')
|
LOGIN_REQUIRED = config.get('require_auth')
|
||||||
|
|
||||||
application.add_sink(index, '/')
|
application.add_sink(index, '/')
|
||||||
|
|||||||
@@ -284,6 +284,12 @@ var oncall = {
|
|||||||
self.settings.notifications.init();
|
self.settings.notifications.init();
|
||||||
self.updateTitleTag("Notifications");
|
self.updateTitleTag("Notifications");
|
||||||
},
|
},
|
||||||
|
'/user/:user/ical_key': function(){
|
||||||
|
oncall.callbacks.onLogin = $.noop;
|
||||||
|
oncall.callbacks.onLogout = $.noop;
|
||||||
|
self.settings.ical_key.init();
|
||||||
|
self.updateTitleTag("Public Calendar Keys");
|
||||||
|
},
|
||||||
'/query/:query/:fields': function(params){
|
'/query/:query/:fields': function(params){
|
||||||
oncall.callbacks.onLogin = $.noop;
|
oncall.callbacks.onLogin = $.noop;
|
||||||
oncall.callbacks.onLogout = $.noop;
|
oncall.callbacks.onLogout = $.noop;
|
||||||
@@ -2468,7 +2474,7 @@ var oncall = {
|
|||||||
if (context[key] == null){
|
if (context[key] == null){
|
||||||
continue;
|
continue;
|
||||||
} else if (key === 'start' || key === 'end') {
|
} else if (key === 'start' || key === 'end') {
|
||||||
context[key] = moment(context[key] * 1000).format('YYYY/MM/DD hh:mm')
|
context[key] = moment(context[key] * 1000).format('YYYY/MM/DD HH:mm')
|
||||||
} else if (context[key].constructor === Array) {
|
} else if (context[key].constructor === Array) {
|
||||||
for (a in context[key]) {
|
for (a in context[key]) {
|
||||||
oncall.team.audit.formatContext(a);
|
oncall.team.audit.formatContext(a);
|
||||||
@@ -2830,6 +2836,186 @@ var oncall = {
|
|||||||
$form.remove();
|
$form.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ical_key: {
|
||||||
|
data: {
|
||||||
|
$page: $('.content-wrapper'),
|
||||||
|
url: '/api/v0/users/',
|
||||||
|
icalKeyUrl: '/api/v0/ical_key/',
|
||||||
|
pageSource: $('#ical-key-template').html(),
|
||||||
|
settingsSubheaderTemplate: $('#settings-subheader-template').html(),
|
||||||
|
moduleIcalKeyTemplate: $('#module-ical-key-template').html(),
|
||||||
|
moduleIcalKeyCreateTemplate: $('#module-ical-key-create-template').html(),
|
||||||
|
icalKeyCreateForm: '.module-ical-key-create',
|
||||||
|
icalKeyCreateCancel: '.ical-key-create-cancel',
|
||||||
|
icalKeyUserCreateContainer: '.ical-key-user-create-container',
|
||||||
|
icalKeyTeamCreateContainer: '.ical-key-team-create-container',
|
||||||
|
subheaderWrapper: '.subheader-wrapper',
|
||||||
|
icalKeyRow: '.ical-key-row',
|
||||||
|
createIcalKeyUser: '#create-ical-key-user',
|
||||||
|
createIcalKeyTeam: '#create-ical-key-team'
|
||||||
|
},
|
||||||
|
init: function(){
|
||||||
|
Handlebars.registerPartial('settings-subheader', this.data.settingsSubheaderTemplate);
|
||||||
|
Handlebars.registerPartial('ical-key', this.data.moduleIcalKeyTemplate);
|
||||||
|
this.getData();
|
||||||
|
},
|
||||||
|
events: function(){
|
||||||
|
router.updatePageLinks();
|
||||||
|
this.data.$page.on('submit', this.data.icalKeyCreateForm, this.createIcalKey.bind(this));
|
||||||
|
this.data.$page.on('click', this.data.icalKeyCreateCancel, this.createIcalKeyCancel.bind(this));
|
||||||
|
this.data.$page.on('click', this.data.createIcalKeyUser, this.createIcalKeyUser.bind(this));
|
||||||
|
this.data.$page.on('click', this.data.createIcalKeyTeam, this.createIcalKeyTeam.bind(this));
|
||||||
|
},
|
||||||
|
getData: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var icalKeyData = {
|
||||||
|
userKeys: [],
|
||||||
|
teamKeys: [],
|
||||||
|
name: oncall.data.user,
|
||||||
|
teams: []
|
||||||
|
};
|
||||||
|
this.data.icalKeyData = icalKeyData;
|
||||||
|
|
||||||
|
if (oncall.data.user) {
|
||||||
|
$.when(
|
||||||
|
$.get(this.data.icalKeyUrl + 'requester/' + oncall.data.user),
|
||||||
|
$.get(this.data.url + oncall.data.user + '/teams')
|
||||||
|
).done(function(icalKeys, teamsData){
|
||||||
|
icalKeys = icalKeys[0];
|
||||||
|
for (var i = 0; i < icalKeys.length; i++) {
|
||||||
|
icalKeys[i].time_created = moment(icalKeys[i].time_created * 1000).format('YYYY/MM/DD HH:mm');
|
||||||
|
if (icalKeys[i].type === 'user')
|
||||||
|
icalKeyData.userKeys.push(icalKeys[i]);
|
||||||
|
else if (icalKeys[i].type === 'team') {
|
||||||
|
icalKeyData.teamKeys.push(icalKeys[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icalKeyData.teams = teamsData[0];
|
||||||
|
|
||||||
|
self.renderPage.call(self, icalKeyData);
|
||||||
|
}).fail(function(){
|
||||||
|
// we need to handle failure because icalKeys promise return 404 when no key exists
|
||||||
|
self.renderPage.call(self, icalKeyData);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.navigate('/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderPage: function(data){
|
||||||
|
var template = Handlebars.compile(this.data.pageSource);
|
||||||
|
|
||||||
|
this.data.$page.html(template(data));
|
||||||
|
this.events();
|
||||||
|
},
|
||||||
|
createIcalKeyUser: function(e, data){
|
||||||
|
var template = Handlebars.compile(this.data.moduleIcalKeyCreateTemplate),
|
||||||
|
$container = $(e.target).parents().find(this.data.icalKeyUserCreateContainer);
|
||||||
|
|
||||||
|
var userCreateData = {
|
||||||
|
createType: 'user',
|
||||||
|
icalKeyOptions: [this.data.icalKeyData.name]
|
||||||
|
};
|
||||||
|
$container.html(template(userCreateData));
|
||||||
|
},
|
||||||
|
createIcalKeyTeam: function(e, data){
|
||||||
|
var template = Handlebars.compile(this.data.moduleIcalKeyCreateTemplate),
|
||||||
|
$container = $(e.target).parents().find(this.data.icalKeyTeamCreateContainer);
|
||||||
|
|
||||||
|
var teamCreateData = {
|
||||||
|
createType: 'team',
|
||||||
|
icalKeyOptions: this.data.icalKeyData.teams
|
||||||
|
};
|
||||||
|
$container.html(template(teamCreateData));
|
||||||
|
},
|
||||||
|
createIcalKey: function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
$form = $(e.target),
|
||||||
|
$cta = $form.find('.ical-key-create-save'),
|
||||||
|
createType = $form.data('type'),
|
||||||
|
// we cannot trim the name here because there are team names ending in space
|
||||||
|
createName = $form.find('.ical-key-create-name').val(),
|
||||||
|
url = this.data.icalKeyUrl + createType + '/' + createName;
|
||||||
|
|
||||||
|
if ((createType === 'user' || createType === 'team') && createName) {
|
||||||
|
$cta.addClass('loading disabled').prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
dataType: 'html'
|
||||||
|
}).done(function(data){
|
||||||
|
$form.remove();
|
||||||
|
self.getData();
|
||||||
|
}).fail(function(data){
|
||||||
|
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Delete failed.';
|
||||||
|
oncall.alerts.createAlert(error, 'danger');
|
||||||
|
}).always(function(){
|
||||||
|
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createIcalKeyCancel: function(e, $caller){
|
||||||
|
var $form = $(e.target).parents(this.data.icalKeyCreateForm);
|
||||||
|
$form.remove();
|
||||||
|
},
|
||||||
|
updateIcalKey: function($modal, $caller){
|
||||||
|
var self = this,
|
||||||
|
$cta = $modal.find('.modal-cta'),
|
||||||
|
ical_type = $caller.attr('data-ical-type'),
|
||||||
|
ical_name = $caller.attr('data-ical-name'),
|
||||||
|
url = this.data.icalKeyUrl + ical_type + '/' + ical_name;
|
||||||
|
|
||||||
|
if ((ical_type === 'user' || ical_type === 'team') && ical_name) {
|
||||||
|
$cta.addClass('loading disabled').prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
dataType: 'html'
|
||||||
|
}).done(function(data){
|
||||||
|
$modal.modal('hide');
|
||||||
|
self.getData();
|
||||||
|
}).fail(function(data){
|
||||||
|
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Delete failed.';
|
||||||
|
oncall.alerts.createAlert(error, 'danger');
|
||||||
|
}).always(function(){
|
||||||
|
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$modal.modal('hide');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteIcalKey: function($modal, $caller){
|
||||||
|
var self = this,
|
||||||
|
$cta = $modal.find('.modal-cta'),
|
||||||
|
key = $caller.attr('data-ical-key'),
|
||||||
|
url = this.data.icalKeyUrl + 'key/' + key;
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
$cta.addClass('loading disabled').prop('disabled', true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'DELETE',
|
||||||
|
url: url,
|
||||||
|
dataType: 'html'
|
||||||
|
}).done(function(){
|
||||||
|
$modal.modal('hide');
|
||||||
|
self.getData();
|
||||||
|
}).fail(function(data){
|
||||||
|
var error = oncall.isJson(data.responseText) ? JSON.parse(data.responseText).description : data.responseText || 'Delete failed.';
|
||||||
|
oncall.alerts.createAlert(error, 'danger');
|
||||||
|
}).always(function(){
|
||||||
|
$cta.removeClass('loading disabled').prop('disabled', false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$modal.modal('hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
@@ -3036,6 +3222,10 @@ var oncall = {
|
|||||||
return str.replace('#', '');
|
return str.replace('#', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper('capitalize', function(str){
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
});
|
||||||
|
|
||||||
Handlebars.registerHelper('friendlyScheduler', function(str){
|
Handlebars.registerHelper('friendlyScheduler', function(str){
|
||||||
if (str ==='no-skip-matching') {
|
if (str ==='no-skip-matching') {
|
||||||
return 'Default (allow duplicate)';
|
return 'Default (allow duplicate)';
|
||||||
|
|||||||
@@ -1416,6 +1416,7 @@
|
|||||||
<ul class="container-fluid">
|
<ul class="container-fluid">
|
||||||
<li {{#isEqual page "settings"}}class="active"{{/isEqual}} href="/user/{{username}}" data-navigo> Settings </li>
|
<li {{#isEqual page "settings"}}class="active"{{/isEqual}} href="/user/{{username}}" data-navigo> Settings </li>
|
||||||
<li {{#isEqual page "notifications"}}class="active"{{/isEqual}} href="/user/{{username}}/notifications" data-navigo> Notifications </li>
|
<li {{#isEqual page "notifications"}}class="active"{{/isEqual}} href="/user/{{username}}/notifications" data-navigo> Notifications </li>
|
||||||
|
<li {{#isEqual page "ical_key"}}class="active"{{/isEqual}} href="/user/{{username}}/ical_key" data-navigo> Public Calendar Keys </li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</script>
|
</script>
|
||||||
@@ -1693,6 +1694,81 @@
|
|||||||
</form>
|
</form>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!--// **********************
|
||||||
|
Public Calendar Key Page
|
||||||
|
*********************** //-->
|
||||||
|
|
||||||
|
<script id="ical-key-template" type="text/x-handlebars-template">
|
||||||
|
<div class="subheader-wrapper">
|
||||||
|
{{>settings-subheader username=name page="ical_key"}}
|
||||||
|
</div>
|
||||||
|
<div class="main container-fluid">
|
||||||
|
<!-- user calendar keys -->
|
||||||
|
{{>ical-key key_type="user" keys=userKeys}}
|
||||||
|
<!-- team calendar keys -->
|
||||||
|
{{>ical-key key_type="team" keys=teamKeys}}
|
||||||
|
<h4 class="module-heading">How to subscribe to your calendars</h4>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
From your personal calendar application, subscribe to the following URL, followed by slash, and followed by a calendar key above. Or copy the link address of a key above.
|
||||||
|
{% endraw %} <pre>{{public_calendar_base_url}}</pre> {% raw %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For example, if the key is abc-123, then your calendar subscription URL will be:
|
||||||
|
{% endraw %} <pre>{{public_calendar_base_url}}/abc-123</pre> {% raw %}
|
||||||
|
</p>
|
||||||
|
{% endraw %} <p>{{public_calendar_additional_message}}</p> {% raw %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- // ical key view partial -->
|
||||||
|
<script id="module-ical-key-template" type="text/x-handlebars-template">
|
||||||
|
<h3 class="module-heading"> {{capitalize key_type}} Calendar Keys <button id="create-ical-key-{{key_type}}" class="btn btn-primary pull-right">+ {{capitalize key_type}} Ical Key</button></h3>
|
||||||
|
<div class="ical-key-{{key_type}}-create-container">
|
||||||
|
<!--// create form renders here -->
|
||||||
|
</div>
|
||||||
|
<div class="module">
|
||||||
|
<table width="100%" class="table table-striped">
|
||||||
|
<thead><th>{{capitalize key_type}} Name</th><th>Key</th><th>Time Created</th></thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each keys}}
|
||||||
|
<tr>
|
||||||
|
<td><span>{{this.name}}</span></td>
|
||||||
|
<td><span><a href="{% endraw %}{{public_calendar_base_url}}{% raw %}/{{this.key}}">{{this.key}}</a></span></td>
|
||||||
|
<td><span>{{this.time_created}}</span></td>
|
||||||
|
<td><button id="update-ical-key" class="btn btn-warning btn-sm pull-right" type="button" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.settings.ical_key.updateIcalKey" data-modal-title="Regenerate Ical Key for {{this.type}} {{this.name}}" data-modal-content="Regenerating an ical key will invalidate the existing key." data-ical-type="{{this.type}}" data-ical-name="{{this.name}}">Regenerate</button></td>
|
||||||
|
<td><button id="delete-ical-key" class="btn btn-danger btn-sm pull-right" type="button" data-toggle="modal" data-target="#confirm-action-modal" data-modal-action="oncall.settings.ical_key.deleteIcalKey" data-modal-title="Delete Ical Key for {{this.type}} {{this.name}}" data-modal-content="Invalidate key {{this.key}}?" data-ical-key="{{this.key}}">Delete</button></td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- // ical key create partial -->
|
||||||
|
<script id="module-ical-key-create-template" type="text/x-handlebars-template">
|
||||||
|
<form class="module module-card module-ical-key-create" data-type="{{createType}}">
|
||||||
|
<div class="module-heading module-card-heading border-bottom">
|
||||||
|
<h4>Create a new {{createType}} calendar key</h4>
|
||||||
|
</div>
|
||||||
|
<div class="ical-key-create-body">
|
||||||
|
<input class="form-control ical-key-create-name" type="text" list="ical-key-options" />
|
||||||
|
<datalist id="ical-key-options">
|
||||||
|
{{#each icalKeyOptions}}
|
||||||
|
<option value="{{this}}">{{this}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div class="border-top notification-actions clearfix">
|
||||||
|
<div class="pull-right">
|
||||||
|
<button class="btn btn-primary ical-key-create-save" type="submit"><span class="btn-text">Save</span> <i class="loader loader-small"></i> </button>
|
||||||
|
<button class="btn btn-blue ical-key-create-cancel" type="button"> Cancel </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</script>
|
||||||
|
|
||||||
<!--// **********************
|
<!--// **********************
|
||||||
Errors Page
|
Errors Page
|
||||||
*********************** //-->
|
*********************** //-->
|
||||||
|
|||||||
Reference in New Issue
Block a user