diff --git a/configs/config.yaml b/configs/config.yaml index da1c499..9abfd74 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -148,6 +148,13 @@ index_content_setting: missing_number_note: 'No number' 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 iris_plan_integration: activated: True diff --git a/src/oncall/ui/__init__.py b/src/oncall/ui/__init__.py index 1042b77..6ed9500 100644 --- a/src/oncall/ui/__init__.py +++ b/src/oncall/ui/__init__.py @@ -88,6 +88,8 @@ def index(req, resp): header_color=HEADER_COLOR, iris_plan_settings=IRIS_PLAN_SETTINGS, 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'], timezones=SUPPORTED_TIMEZONES ) @@ -134,11 +136,15 @@ def init(application, config): global HEADER_COLOR global IRIS_PLAN_SETTINGS global USERCONTACT_UI_READONLY + global PUBLIC_CALENDAR_BASE_URL + global PUBLIC_CALENDAR_ADDITIONAL_MESSAGE global LOGIN_REQUIRED SLACK_INSTANCE = config.get('slack_instance') HEADER_COLOR = config.get('header_color', '#3a3a3a') IRIS_PLAN_SETTINGS = config.get('iris_plan_integration') 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') application.add_sink(index, '/') diff --git a/src/oncall/ui/static/js/oncall.js b/src/oncall/ui/static/js/oncall.js index fbce3f0..61868c6 100644 --- a/src/oncall/ui/static/js/oncall.js +++ b/src/oncall/ui/static/js/oncall.js @@ -284,6 +284,12 @@ var oncall = { self.settings.notifications.init(); 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){ oncall.callbacks.onLogin = $.noop; oncall.callbacks.onLogout = $.noop; @@ -2468,7 +2474,7 @@ var oncall = { if (context[key] == null){ continue; } 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) { for (a in context[key]) { oncall.team.audit.formatContext(a); @@ -2830,6 +2836,186 @@ var oncall = { $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: { @@ -3036,6 +3222,10 @@ var oncall = { return str.replace('#', ''); }); + Handlebars.registerHelper('capitalize', function(str){ + return str.charAt(0).toUpperCase() + str.slice(1); + }); + Handlebars.registerHelper('friendlyScheduler', function(str){ if (str ==='no-skip-matching') { return 'Default (allow duplicate)'; diff --git a/src/oncall/ui/templates/index.html b/src/oncall/ui/templates/index.html index 08cb3af..3d9b534 100644 --- a/src/oncall/ui/templates/index.html +++ b/src/oncall/ui/templates/index.html @@ -1416,6 +1416,7 @@