You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
469 lines
17 KiB
469 lines
17 KiB
/**
|
|
*
|
|
// Avoid server side code :
|
|
// https://github.com/ipython/ipython/issues/2780
|
|
*
|
|
* This essentially boils down to the following:
|
|
* Github authentication requires some server-side code for any 'app' which
|
|
* wants to authenticate over the Github API.
|
|
* When registering an app with Github, Github provides the app with what they
|
|
* call a 'client secret'.
|
|
* The client secret is then incorporated into the app, and the app sends it to
|
|
* Github as part of the authentication process, thus proving to Github's
|
|
* servers that the communicating app was written by someone with appropriate
|
|
* credentials.
|
|
*
|
|
* The issue with writing a single Github app for Gist-ing notebooks, is that
|
|
* it would need to include such a client secret. Since this would be part of
|
|
* the extension source code, anyone could use the client secret, potentially
|
|
* gaining the permissions that any given user has granted to the app.
|
|
*
|
|
* As a result, we only support:
|
|
* - anonymous (non-authenticated) API usage
|
|
* - client-side authentication using a personal access token
|
|
* (see https://github.com/settings/tokens)
|
|
*/
|
|
|
|
define([
|
|
'jquery',
|
|
'base/js/namespace',
|
|
'base/js/dialog'
|
|
], function (
|
|
$,
|
|
Jupyter,
|
|
dialog
|
|
) {
|
|
"use strict";
|
|
|
|
// define default values for config parameters
|
|
var params = {
|
|
gist_it_default_to_public: false,
|
|
gist_it_personal_access_token: '',
|
|
github_endpoint: 'github.com'
|
|
};
|
|
|
|
var initialize = function () {
|
|
update_params();
|
|
Jupyter.toolbar.add_buttons_group([
|
|
Jupyter.keyboard_manager.actions.register ({
|
|
help : 'Create/Edit Gist of Notebook',
|
|
icon : 'fa-github',
|
|
handler: show_gist_editor_modal
|
|
}, 'create-gist-from-notebook', 'gist_it')
|
|
]);
|
|
};
|
|
|
|
// update params with any specified in the server's config file
|
|
var update_params = function() {
|
|
var config = Jupyter.notebook.config;
|
|
for (var key in params) {
|
|
if (config.data.hasOwnProperty(key))
|
|
params[key] = config.data[key];
|
|
}
|
|
default_metadata.data.public = Boolean(config.data.gist_it_default_to_public);
|
|
};
|
|
|
|
var default_metadata = {
|
|
id: '',
|
|
data: {
|
|
description: Jupyter.notebook.notebook_path,
|
|
public: false
|
|
}
|
|
};
|
|
|
|
function ensure_default_metadata () {
|
|
Jupyter.notebook.metadata.gist = $.extend(
|
|
true, // deep-copy
|
|
default_metadata, //defaults
|
|
Jupyter.notebook.metadata.gist // overrides
|
|
);
|
|
}
|
|
|
|
var add_auth_token = function add_auth_token (xhr) {
|
|
var token = '';
|
|
if (params.gist_it_personal_access_token !== '') {
|
|
token = params.gist_it_personal_access_token;
|
|
}
|
|
if (token !== '') {
|
|
xhr.setRequestHeader("Authorization", "token " + token);
|
|
}
|
|
};
|
|
|
|
function build_alert(alert_class) {
|
|
return $('<div/>')
|
|
.addClass('alert alert-dismissable')
|
|
.addClass(alert_class)
|
|
.append(
|
|
$('<button class="close" type="button" data-dismiss="alert" aria-label="Close"/>')
|
|
.append($('<span aria-hidden="true"/>').html('×'))
|
|
);
|
|
}
|
|
|
|
function gist_error (jqXHR, textStatus, errorThrown) {
|
|
console.log('github ajax error:', jqXHR, textStatus, errorThrown);
|
|
var alert = build_alert('alert-danger')
|
|
.hide()
|
|
.append(
|
|
$('<p/>').text('The ajax request to Github went wrong:')
|
|
)
|
|
.append(
|
|
$('<pre/>').text(jqXHR.responseJSON ? JSON.stringify(jqXHR.responseJSON, null, 2) : errorThrown)
|
|
);
|
|
$('#gist_modal').find('.modal-body').append(alert);
|
|
alert.slideDown('fast');
|
|
}
|
|
|
|
function gist_success (response, textStatus, jqXHR) {
|
|
// if (Jupyter.notebook.metadata.gist.id === response.id) return;
|
|
|
|
Jupyter.notebook.metadata.gist.id = response.id;
|
|
Jupyter.notebook.metadata._draft = $.extend(
|
|
true, // deep copy
|
|
Jupyter.notebook.metadata._draft, // defaults
|
|
{nbviewer_url: response.html_url} // overrides
|
|
);
|
|
|
|
var d = new Date();
|
|
var msg_head = d.toLocaleString() + ': Gist ';
|
|
var msg_tail = response.history.length === 1 ? ' published' : ' updated to revision ' + response.history.length;
|
|
var alert = build_alert('alert-success')
|
|
.hide()
|
|
.append(msg_head)
|
|
.append(
|
|
$('<a/>')
|
|
.attr('href', response.html_url)
|
|
.attr('target', '_blank')
|
|
.text(response.id)
|
|
)
|
|
.append(msg_tail);
|
|
$('#gist_modal').find('.modal-body').append(alert);
|
|
alert.slideDown('fast');
|
|
}
|
|
|
|
function gist_id_updated_callback(gist_editor) {
|
|
if (gist_editor === undefined) gist_editor = $('#gist_editor');
|
|
|
|
var id_input = gist_editor.find('#gist_id');
|
|
var id = id_input.val();
|
|
|
|
var help_block = gist_editor.find('#gist_id ~ .help-block');
|
|
var help_block_base_text = 'Set the gist id to update an existing gist, ' +
|
|
'or leave blank to create a new one.';
|
|
|
|
var gist_it_button = $('#gist_modal').find('.btn-primary');
|
|
|
|
id_input.parent()
|
|
.removeClass('has-success has-error has-warning')
|
|
.find('#gist_id ~ .form-control-feedback > i.fa')
|
|
.removeClass('fa-pencil-square fa-exclamation-circle fa-question-circle');
|
|
|
|
if (id === '') {
|
|
$('#gist_id ~ .form-control-feedback > i.fa')
|
|
.addClass('fa-plus-circle');
|
|
help_block.html(
|
|
'<p>' + help_block_base_text + '</p>' +
|
|
'<p><i class="fa fa-plus-circle"></i> a new gist will be created</p>'
|
|
);
|
|
gist_it_button.prop('disabled', false);
|
|
}
|
|
else {
|
|
$('#gist_id ~ .form-control-feedback > i.fa')
|
|
.addClass('fa-circle-o-notch fa-spin');
|
|
// List commits as a way of checking whether the gist exists.
|
|
// Listing commits appears to give the most concise response.
|
|
var github_endpoint = params.github_endpoint !== '' ? params.github_endpoint : 'github.com';
|
|
|
|
$.ajax({
|
|
url: 'https://api.'+ github_endpoint +'/gists/' + id + '/commits',
|
|
dataType: 'json',
|
|
beforeSend: add_auth_token,
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
jqXHR.errorThrown = errorThrown;
|
|
},
|
|
complete: function(jqXHR, textStatus) {
|
|
var success = textStatus === 'success';
|
|
var error = !success && jqXHR.status === 404 && jqXHR.responseJSON !== undefined;
|
|
var warning = !success && !error;
|
|
|
|
var help_block_html = '<p>' + help_block_base_text + '</p>';
|
|
|
|
gist_it_button.prop('disabled', error);
|
|
if (success) {
|
|
var single = (jqXHR.responseJSON.length === 1);
|
|
help_block_html += '<p>' +
|
|
'<i class="fa fa-pencil-square"></i>' +
|
|
' gist ' +
|
|
'<a href="https://'+ github_endpoint + '/gist/' + id +
|
|
'" target="_blank">' + id + '</a> will be updated' +
|
|
' (' + jqXHR.responseJSON.length +
|
|
' revision' + (single ? '' : 's') +
|
|
' exist' + (single ? 's' : '') + ' so far)' +
|
|
'</p>';
|
|
}
|
|
else if (error) {
|
|
help_block_html += '<p>' +
|
|
'<i class="fa fa-exclamation-circle"></i>' +
|
|
' no gist exists with the specified id (given current access token)'+
|
|
'</p>';
|
|
}
|
|
else {
|
|
help_block_html += '<p>' +
|
|
'<i class="fa fa-question-circle"></i>' +
|
|
' can\'t list commits for the specified gist id - you may have problems updating it!' +
|
|
'</p>';
|
|
help_block_html += '<p>The ajax request to Github went wrong:<p/>' +
|
|
'<pre>';
|
|
if (jqXHR.responseJSON) {
|
|
help_block_html += JSON.stringify(jqXHR.responseJSON, null, 2);
|
|
}
|
|
else {
|
|
help_block_html += jqXHR.errorThrown || textStatus;
|
|
}
|
|
help_block_html += '</pre>';
|
|
console.log('non-404 github ajax error:', jqXHR, textStatus);
|
|
}
|
|
help_block.html(help_block_html);
|
|
|
|
id_input.parent()
|
|
.toggleClass('has-success', success)
|
|
.toggleClass('has-error', error)
|
|
.toggleClass('has-warning', warning)
|
|
.find('#gist_id ~ .form-control-feedback > i.fa')
|
|
.removeClass('fa-circle-o-notch fa-spin')
|
|
.toggleClass('fa-pencil-square', success)
|
|
.toggleClass('fa-exclamation-circle', error)
|
|
.toggleClass('fa-question-circle', warning);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function update_gist_editor (gist_editor) {
|
|
if (gist_editor === undefined) gist_editor = $('#gist_editor');
|
|
|
|
var id_input = gist_editor.find('#gist_id');
|
|
|
|
var have_auth = params.gist_it_personal_access_token !== '';
|
|
var id = '';
|
|
var is_public = true;
|
|
if (have_auth) {
|
|
id = Jupyter.notebook.metadata.gist.id;
|
|
is_public = Jupyter.notebook.metadata.gist.data.public;
|
|
id_input.val(id);
|
|
}
|
|
id_input.closest('.form-group').toggle(have_auth);
|
|
|
|
gist_editor.find('#gist_public')
|
|
.prop('checked', is_public)
|
|
.prop('readonly', !have_auth);
|
|
|
|
gist_editor.find('#gist_description')
|
|
.val(Jupyter.notebook.metadata.gist.data.description);
|
|
|
|
if (have_auth) {
|
|
gist_id_updated_callback(gist_editor);
|
|
}
|
|
}
|
|
|
|
function build_gist_editor () {
|
|
ensure_default_metadata();
|
|
|
|
var gist_editor = $('#gist_editor');
|
|
|
|
if (gist_editor.length > 0) return gist_editor;
|
|
|
|
gist_editor = $('<div/>').attr('id', 'gist_editor').append(controls);
|
|
|
|
var id = params.gist_it_personal_access_token !== '' ? Jupyter.notebook.metadata.gist.id : '';
|
|
var controls = $('<form/>')
|
|
.appendTo(gist_editor)
|
|
.addClass('form-horizontal');
|
|
|
|
$('<div/>')
|
|
.addClass('has-feedback')
|
|
.hide()
|
|
.appendTo(controls)
|
|
.append(
|
|
$('<label/>')
|
|
.attr('for', 'gist_id')
|
|
.text('Gist id')
|
|
)
|
|
.append(
|
|
$('<input/>')
|
|
.addClass('form-control')
|
|
.attr('id', 'gist_id')
|
|
.val(Jupyter.notebook.metadata.gist.id)
|
|
)
|
|
.append(
|
|
$('<span/>')
|
|
.addClass('form-control-feedback')
|
|
.append(
|
|
$('<i/>')
|
|
.addClass('fa fa-lg')
|
|
)
|
|
)
|
|
.append(
|
|
$('<span/>')
|
|
.addClass('help-block')
|
|
);
|
|
$('<div/>')
|
|
.appendTo(controls)
|
|
.append(
|
|
$('<div/>')
|
|
.addClass('checkbox')
|
|
.append(
|
|
$('<label>')
|
|
.text('Make the gist public')
|
|
.prepend(
|
|
$('<input/>')
|
|
.attr('type', 'checkbox')
|
|
.attr('id', 'gist_public')
|
|
.prop('checked', Jupyter.notebook.metadata.gist.data.public)
|
|
.prop('readonly', id === '')
|
|
)
|
|
)
|
|
)
|
|
.append(
|
|
$('<label/>')
|
|
.attr('for', 'gist_public')
|
|
.text('public')
|
|
);
|
|
$('<div/>')
|
|
.appendTo(controls)
|
|
.append(
|
|
$('<label/>')
|
|
.attr('for', 'gist_description')
|
|
.text('description')
|
|
)
|
|
.append(
|
|
$('<input/>')
|
|
.addClass('form-control')
|
|
.attr('id', 'gist_description')
|
|
.attr('type', 'textarea')
|
|
.val(Jupyter.notebook.metadata.gist.data.description)
|
|
);
|
|
|
|
var form_groups = controls.children('div').addClass('form-group');
|
|
form_groups
|
|
.children('label')
|
|
.addClass('col-sm-2 control-label')
|
|
.css('padding-right', '1em');
|
|
form_groups
|
|
.each(function (index, elem) {
|
|
$('<div/>')
|
|
.appendTo(elem)
|
|
.addClass('col-sm-10')
|
|
.append($(elem).children(':not(label)'));
|
|
});
|
|
|
|
update_gist_editor(gist_editor);
|
|
|
|
// bind events for id changing
|
|
var id_input = gist_editor.find('#gist_id');
|
|
// Save current value of element
|
|
id_input.data('oldVal', id_input.val());
|
|
// Look for changes in the value
|
|
id_input.bind("change click keyup input paste", function(event) {
|
|
// If value has changed...
|
|
if (id_input.data('oldVal') !== id_input.val()) {
|
|
// Updated stored value
|
|
id_input.data('oldVal', id_input.val());
|
|
// Do action
|
|
gist_id_updated_callback(gist_editor);
|
|
}
|
|
});
|
|
|
|
return gist_editor;
|
|
}
|
|
|
|
function show_gist_editor_modal () {
|
|
var modal;
|
|
modal = dialog.modal({
|
|
show: false,
|
|
title: 'Share on Github',
|
|
notebook: Jupyter.notebook,
|
|
keyboard_manager: Jupyter.notebook.keyboard_manager,
|
|
body: build_gist_editor(),
|
|
buttons: {
|
|
' Gist it!': {
|
|
class : 'btn-primary',
|
|
click: function() {
|
|
modal.find('.btn').prop('disabled', true);
|
|
var new_data = {
|
|
public: $('#gist_public').prop('checked'),
|
|
description: $('#gist_description').val()
|
|
};
|
|
$.extend(
|
|
true,
|
|
Jupyter.notebook.metadata.gist.data,
|
|
new_data
|
|
);
|
|
// prevent the modal from closing. See github.com/twbs/bootstrap/issues/1202
|
|
modal.data('bs.modal').isShown = false;
|
|
var spinner = modal.find('.btn-primary .fa-github').addClass('fa-spin');
|
|
make_gist(function (jqXHR, textStatus) {
|
|
modal.find('.btn').prop('disabled', false);
|
|
// allow the modal to close again. See github.com/twbs/bootstrap/issues/1202
|
|
modal.data('bs.modal').isShown = true;
|
|
spinner.removeClass('fa-spin');
|
|
});
|
|
}
|
|
},
|
|
done: {}
|
|
}
|
|
})
|
|
.attr('id', 'gist_modal')
|
|
.on('shown.bs.modal', function (evt) {
|
|
var err = modal.find('#gist_id').parent().hasClass('has-error');
|
|
modal.find('.btn-primary').prop('disabled', err);
|
|
});
|
|
|
|
modal.find('.btn-primary').prepend(
|
|
$('<i/>')
|
|
.addClass('fa fa-lg fa-github')
|
|
);
|
|
|
|
modal.modal('show');
|
|
}
|
|
|
|
var make_gist = function make_gist (complete_callback) {
|
|
ensure_default_metadata();
|
|
|
|
var data = $.extend(
|
|
true, // deep-copy
|
|
{ files: {} }, // defaults
|
|
Jupyter.notebook.metadata.gist.data // overrides
|
|
);
|
|
var filename = Jupyter.notebook.notebook_name;
|
|
data.files[filename] = {
|
|
content: JSON.stringify(Jupyter.notebook.toJSON(), null, 2)
|
|
};
|
|
|
|
var id_input = $('#gist_id');
|
|
var id = params.gist_it_personal_access_token !== '' ? id_input.val() : '';
|
|
var method = id ? 'PATCH' : 'POST';
|
|
|
|
var github_endpoint = params.github_endpoint !== '' ? params.github_endpoint : 'github.com';
|
|
// Create/edit the Gist
|
|
$.ajax({
|
|
url: 'https://api.'+ github_endpoint +'/gists' + (id ? '/' + id : ''),
|
|
type: method,
|
|
dataType: 'json',
|
|
data: JSON.stringify(data),
|
|
beforeSend: add_auth_token,
|
|
success: gist_success,
|
|
error: gist_error,
|
|
complete: complete_callback
|
|
});
|
|
};
|
|
|
|
function load_jupyter_extension () {
|
|
return Jupyter.notebook.config.loaded.then(initialize);
|
|
}
|
|
|
|
return {
|
|
load_jupyter_extension: load_jupyter_extension,
|
|
load_ipython_extension: load_jupyter_extension
|
|
};
|
|
});
|