diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index c64ebd7c5..a52f1d6f9 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -136,6 +136,12 @@ class IPythonHandler(AuthenticatedHandler): @property def ws_url(self): return self.settings.get('websocket_url', '') + + @property + def contents_js_source(self): + self.log.debug("Using contents: %s", self.settings.get('contents_js_source', + 'services/contents')) + return self.settings.get('contents_js_source', 'services/contents') #--------------------------------------------------------------- # Manager objects @@ -224,7 +230,8 @@ class IPythonHandler(AuthenticatedHandler): logged_in=self.logged_in, login_available=self.login_available, static_url=self.static_url, - sys_info=sys_info + sys_info=sys_info, + contents_js_source=self.contents_js_source, ) def get_json_body(self): diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index 22261f3e4..16f9a7068 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -563,6 +563,22 @@ define([ ); }; + /** Error type for wrapped XHR errors. */ + var XHR_ERROR = 'XhrError'; + + /** + * Wraps an AJAX error as an Error object. + */ + var wrap_ajax_error = function (jqXHR, status, error) { + var wrapped_error = new Error(ajax_error_msg(jqXHR)); + wrapped_error.name = XHR_ERROR; + // provide xhr response + wrapped_error.xhr = jqXHR; + wrapped_error.xhr_status = status; + wrapped_error.xhr_error = error; + return wrapped_error; + } + var utils = { regex_split : regex_split, uuid : uuid, @@ -588,6 +604,8 @@ define([ ajax_error_msg : ajax_error_msg, log_ajax_error : log_ajax_error, requireCodeMirrorMode : requireCodeMirrorMode, + XHR_ERROR : XHR_ERROR, + wrap_ajax_error : wrap_ajax_error }; // Backwards compatability. diff --git a/IPython/html/static/notebook/js/main.js b/IPython/html/static/notebook/js/main.js index 3cf2782e3..486267de9 100644 --- a/IPython/html/static/notebook/js/main.js +++ b/IPython/html/static/notebook/js/main.js @@ -5,6 +5,7 @@ require([ 'base/js/namespace', 'jquery', 'notebook/js/notebook', + 'contents', 'base/js/utils', 'base/js/page', 'notebook/js/layoutmanager', @@ -27,6 +28,7 @@ require([ IPython, $, notebook, + contents, utils, page, layoutmanager, @@ -70,10 +72,14 @@ require([ var save_widget = new savewidget.SaveWidget('span#save_widget', { events: events, keyboard_manager: keyboard_manager}); + var contents = new contents.Contents($.extend({ + events: events}, + common_options)); var notebook = new notebook.Notebook('div#notebook', $.extend({ events: events, keyboard_manager: keyboard_manager, save_widget: save_widget, + contents: contents, config: user_config}, common_options)); var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options); @@ -86,6 +92,7 @@ require([ notebook: notebook}); var menubar = new menubar.MenuBar('#menubar', $.extend({ notebook: notebook, + contents: contents, layout_manager: layout_manager, events: events, save_widget: save_widget, @@ -131,6 +138,7 @@ require([ IPython.page = page; IPython.layout_manager = layout_manager; IPython.notebook = notebook; + IPython.contents = contents; IPython.pager = pager; IPython.quick_help = quick_help; IPython.login_widget = login_widget; diff --git a/IPython/html/static/notebook/js/menubar.js b/IPython/html/static/notebook/js/menubar.js index c7e359583..fc6f26523 100644 --- a/IPython/html/static/notebook/js/menubar.js +++ b/IPython/html/static/notebook/js/menubar.js @@ -21,6 +21,7 @@ define([ // options: dictionary // Dictionary of keyword arguments. // notebook: Notebook instance + // contents: ContentManager instance // layout_manager: LayoutManager instance // events: $(Events) instance // save_widget: SaveWidget instance @@ -32,6 +33,7 @@ define([ this.base_url = options.base_url || utils.get_body_data("baseUrl"); this.selector = selector; this.notebook = options.notebook; + this.contents = options.contents; this.layout_manager = options.layout_manager; this.events = options.events; this.save_widget = options.save_widget; @@ -85,7 +87,26 @@ define([ // File var that = this; this.element.find('#new_notebook').click(function () { - that.notebook.new_notebook(); + // Create a new notebook in the same path as the current + // notebook's path. + that.contents.new(that.notebook.notebook_path, null, { + ext: ".ipynb", + extra_settings: {async: false}, // So we can open a new window afterwards + success: function (data) { + window.open( + utils.url_join_encode( + that.base_url, 'notebooks', + data.path, data.name + ), '_blank'); + }, + error: function(error) { + dialog.modal({ + title : 'Creating Notebook Failed', + body : "The error was: " + error.message, + buttons : {'OK' : {'class' : 'btn-primary'}} + }); + } + }); }); this.element.find('#open_notebook').click(function () { window.open(utils.url_join_encode( diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 12c0f8b3c..e1f41c749 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -50,6 +50,7 @@ define([ // Dictionary of keyword arguments. // events: $(Events) instance // keyboard_manager: KeyboardManager instance + // contents: Contents instance // save_widget: SaveWidget instance // config: dictionary // base_url : string @@ -61,6 +62,7 @@ define([ this.notebook_name = options.notebook_name; this.events = options.events; this.keyboard_manager = options.keyboard_manager; + this.contents = options.contents; this.save_widget = options.save_widget; this.tooltip = new tooltip.Tooltip(this.events); this.ws_url = options.ws_url; @@ -1860,7 +1862,9 @@ define([ } var data = { cells: cell_array, - metadata : this.metadata + metadata: this.metadata, + nbformat: this.nbformat, + nbformat_minor: this.nbformat_minor }; if (trusted != this.trusted) { this.trusted = trusted; @@ -1904,52 +1908,33 @@ define([ */ Notebook.prototype.save_notebook = function (extra_settings) { // Create a JSON model to be sent to the server. - var model = {}; - model.name = this.notebook_name; - model.path = this.notebook_path; - model.type = 'notebook'; - model.format = 'json'; - model.content = this.toJSON(); - model.content.nbformat = this.nbformat; - model.content.nbformat_minor = this.nbformat_minor; + var model = { + name : this.notebook_name, + path : this.notebook_path, + type : "notebook", + content : this.toJSON() + }; // time the ajax call for autosave tuning purposes. var start = new Date().getTime(); - // We do the call with settings so we can set cache to false. - var settings = { - processData : false, - cache : false, - type : "PUT", - data : JSON.stringify(model), - contentType: 'application/json', - dataType : "json", - success : $.proxy(this.save_notebook_success, this, start), - error : $.proxy(this.save_notebook_error, this) - }; - if (extra_settings) { - for (var key in extra_settings) { - settings[key] = extra_settings[key]; - } - } - this.events.trigger('notebook_saving.Notebook'); - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name - ); - $.ajax(url, settings); + + var that = this; + this.contents.save(this.notebook_path, this.notebook_name, model, { + extra_settings: extra_settings, + success: $.proxy(this.save_notebook_success, this, start), + error: function (error) { + that.events.trigger('notebook_save_failed.Notebook'); + } + }); }; /** * Success callback for saving a notebook. * * @method save_notebook_success - * @param {Integer} start the time when the save request started + * @param {Integer} start Time when the save request start * @param {Object} data JSON representation of a notebook - * @param {String} status Description of response status - * @param {jqXHR} xhr jQuery Ajax object */ - Notebook.prototype.save_notebook_success = function (start, data, status, xhr) { + Notebook.prototype.save_notebook_success = function (start, data) { this.set_dirty(false); if (data.message) { // save succeeded, but validation failed. @@ -2002,18 +1987,6 @@ define([ } } }; - - /** - * Failure callback for saving a notebook. - * - * @method save_notebook_error - * @param {jqXHR} xhr jQuery Ajax object - * @param {String} status Description of response status - * @param {String} error HTTP error message - */ - Notebook.prototype.save_notebook_error = function (xhr, status, error) { - this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]); - }; /** * Explicitly trust the output of this notebook. @@ -2065,124 +2038,47 @@ define([ }); }; - Notebook.prototype.new_notebook = function(){ - var path = this.notebook_path; - var base_url = this.base_url; - var settings = { - processData : false, - cache : false, - type : "POST", - dataType : "json", - async : false, - success : function (data, status, xhr){ - var notebook_name = data.name; - window.open( - utils.url_join_encode( - base_url, - 'notebooks', - path, - notebook_name - ), - '_blank' - ); - }, - error : utils.log_ajax_error, - }; - var url = utils.url_join_encode( - base_url, - 'api/contents', - path - ); - $.ajax(url,settings); - }; - - Notebook.prototype.copy_notebook = function(){ - var path = this.notebook_path; var base_url = this.base_url; - var settings = { - processData : false, - cache : false, - type : "POST", - dataType : "json", - data : JSON.stringify({copy_from : this.notebook_name}), - async : false, - success : function (data, status, xhr) { + this.contents.copy(this.notebook_path, null, this.notebook_name, { + // synchronous so we can open a new window on success + extra_settings: {async: false}, + success: function (data) { window.open(utils.url_join_encode( - base_url, - 'notebooks', - data.path, - data.name + base_url, 'notebooks', data.path, data.name ), '_blank'); }, - error : utils.log_ajax_error, - }; - var url = utils.url_join_encode( - base_url, - 'api/contents', - path - ); - $.ajax(url,settings); + error : utils.log_ajax_error + }); }; - Notebook.prototype.rename = function (nbname) { - var that = this; - if (!nbname.match(/\.ipynb$/)) { - nbname = nbname + ".ipynb"; - } - var data = {name: nbname}; - var settings = { - processData : false, - cache : false, - type : "PATCH", - data : JSON.stringify(data), - dataType: "json", - contentType: 'application/json', - success : $.proxy(that.rename_success, this), - error : $.proxy(that.rename_error, this) - }; - this.events.trigger('rename_notebook.Notebook', data); - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name - ); - $.ajax(url, settings); - }; + Notebook.prototype.rename = function (new_name) { + if (!new_name.match(/\.ipynb$/)) { + new_name = new_name + ".ipynb"; + } - Notebook.prototype.delete = function () { var that = this; - var settings = { - processData : false, - cache : false, - type : "DELETE", - dataType: "json", - error : utils.log_ajax_error, - }; - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name - ); - $.ajax(url, settings); + this.contents.rename(this.notebook_path, this.notebook_name, + this.notebook_path, new_name, { + success: function (json) { + var name = that.notebook_name = json.name; + that.session.rename_notebook(name, json.path); + that.events.trigger('notebook_renamed.Notebook', json); + }, + error: $.proxy(this.rename_error, this) + }); }; - - Notebook.prototype.rename_success = function (json, status, xhr) { - var name = this.notebook_name = json.name; - var path = json.path; - this.session.rename_notebook(name, path); - this.events.trigger('notebook_renamed.Notebook', json); + Notebook.prototype.delete = function () { + this.contents.delete(this.notebook_name, this.notebook_path); }; - Notebook.prototype.rename_error = function (xhr, status, error) { + Notebook.prototype.rename_error = function (error) { var that = this; var dialog_body = $('
').append( $("

").text('This notebook name already exists.') ); - this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]); + this.events.trigger('notebook_rename_failed.Notebook', error); dialog.modal({ notebook: this, keyboard_manager: this.keyboard_manager, @@ -2193,7 +2089,7 @@ define([ "OK": { class: "btn-primary", click: function () { - this.save_widget.rename_notebook({notebook:that}); + that.save_widget.rename_notebook({notebook:that}); }} }, open : function (event, ui) { @@ -2216,26 +2112,13 @@ define([ * @param {String} notebook_name and path A notebook to load */ Notebook.prototype.load_notebook = function (notebook_name, notebook_path) { - var that = this; this.notebook_name = notebook_name; this.notebook_path = notebook_path; - // We do the call with settings so we can set cache to false. - var settings = { - processData : false, - cache : false, - type : "GET", - dataType : "json", - success : $.proxy(this.load_notebook_success,this), - error : $.proxy(this.load_notebook_error,this), - }; this.events.trigger('notebook_loading.Notebook'); - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name - ); - $.ajax(url, settings); + this.contents.load(notebook_path, notebook_name, { + success: $.proxy(this.load_notebook_success, this), + error: $.proxy(this.load_notebook_error, this) + }); }; /** @@ -2245,10 +2128,8 @@ define([ * * @method load_notebook_success * @param {Object} data JSON representation of a notebook - * @param {String} status Description of response status - * @param {jqXHR} xhr jQuery Ajax object */ - Notebook.prototype.load_notebook_success = function (data, status, xhr) { + Notebook.prototype.load_notebook_success = function (data) { var failed; try { this.fromJSON(data); @@ -2393,20 +2274,18 @@ define([ * Failure callback for loading a notebook from the server. * * @method load_notebook_error - * @param {jqXHR} xhr jQuery Ajax object - * @param {String} status Description of response status - * @param {String} error HTTP error message - */ - Notebook.prototype.load_notebook_error = function (xhr, status, error) { - this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]); - utils.log_ajax_error(xhr, status, error); - var msg = $("

"); - if (xhr.status === 400) { - msg.text(utils.ajax_error_msg(xhr)); - } else if (xhr.status === 500) { - msg.text("An unknown error occurred while loading this notebook. " + + * @param {Error} error + */ + Notebook.prototype.load_notebook_error = function (error) { + this.events.trigger('notebook_load_failed.Notebook', error); + var msg; + if (error.name = utils.XHR_ERROR && error.xhr.status === 500) { + utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error); + msg = "An unknown error occurred while loading this notebook. " + "This version can load notebook formats " + - "v" + this.nbformat + " or earlier. See the server log for details."); + "v" + this.nbformat + " or earlier. See the server log for details."; + } else { + msg = error.message; } dialog.modal({ notebook: this, @@ -2459,18 +2338,13 @@ define([ * @method list_checkpoints */ Notebook.prototype.list_checkpoints = function () { - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name, - 'checkpoints' - ); - $.get(url).done( - $.proxy(this.list_checkpoints_success, this) - ).fail( - $.proxy(this.list_checkpoints_error, this) - ); + var that = this; + this.contents.list_checkpoints(this.notebook_path, this.notebook_name, { + success: $.proxy(this.list_checkpoints_success, this), + error: function(error) { + that.events.trigger('list_checkpoints_failed.Notebook'); + } + }); }; /** @@ -2478,10 +2352,8 @@ define([ * * @method list_checkpoint_success * @param {Object} data JSON representation of a checkpoint - * @param {String} status Description of response status - * @param {jqXHR} xhr jQuery Ajax object */ - Notebook.prototype.list_checkpoints_success = function (data, status, xhr) { + Notebook.prototype.list_checkpoints_success = function (data) { data = $.parseJSON(data); this.checkpoints = data; if (data.length) { @@ -2492,36 +2364,19 @@ define([ this.events.trigger('checkpoints_listed.Notebook', [data]); }; - /** - * Failure callback for listing a checkpoint. - * - * @method list_checkpoint_error - * @param {jqXHR} xhr jQuery Ajax object - * @param {String} status Description of response status - * @param {String} error_msg HTTP error message - */ - Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) { - this.events.trigger('list_checkpoints_failed.Notebook'); - }; - /** * Create a checkpoint of this notebook on the server from the most recent save. * * @method create_checkpoint */ Notebook.prototype.create_checkpoint = function () { - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name, - 'checkpoints' - ); - $.post(url).done( - $.proxy(this.create_checkpoint_success, this) - ).fail( - $.proxy(this.create_checkpoint_error, this) - ); + var that = this; + this.contents.create_checkpoint(this.notebook_path, this.notebook_name, { + success: $.proxy(this.create_checkpoint_success, this), + error: function (error) { + that.events.trigger('checkpoint_failed.Notebook'); + } + }); }; /** @@ -2529,27 +2384,13 @@ define([ * * @method create_checkpoint_success * @param {Object} data JSON representation of a checkpoint - * @param {String} status Description of response status - * @param {jqXHR} xhr jQuery Ajax object */ - Notebook.prototype.create_checkpoint_success = function (data, status, xhr) { + Notebook.prototype.create_checkpoint_success = function (data) { data = $.parseJSON(data); this.add_checkpoint(data); this.events.trigger('checkpoint_created.Notebook', data); }; - /** - * Failure callback for creating a checkpoint. - * - * @method create_checkpoint_error - * @param {jqXHR} xhr jQuery Ajax object - * @param {String} status Description of response status - * @param {String} error_msg HTTP error message - */ - Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) { - this.events.trigger('checkpoint_failed.Notebook'); - }; - Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) { var that = this; checkpoint = checkpoint || this.last_checkpoint; @@ -2599,46 +2440,26 @@ define([ */ Notebook.prototype.restore_checkpoint = function (checkpoint) { this.events.trigger('notebook_restoring.Notebook', checkpoint); - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name, - 'checkpoints', - checkpoint - ); - $.post(url).done( - $.proxy(this.restore_checkpoint_success, this) - ).fail( - $.proxy(this.restore_checkpoint_error, this) - ); + var that = this; + this.contents.restore_checkpoint(this.notebook_path, this.notebook_name, + checkpoint, { + success: $.proxy(this.restore_checkpoint_success, this), + error: function (error) { + that.events.trigger('checkpoint_restore_failed.Notebook'); + } + }); }; /** * Success callback for restoring a notebook to a checkpoint. * * @method restore_checkpoint_success - * @param {Object} data (ignored, should be empty) - * @param {String} status Description of response status - * @param {jqXHR} xhr jQuery Ajax object */ - Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) { + Notebook.prototype.restore_checkpoint_success = function () { this.events.trigger('checkpoint_restored.Notebook'); this.load_notebook(this.notebook_name, this.notebook_path); }; - /** - * Failure callback for restoring a notebook to a checkpoint. - * - * @method restore_checkpoint_error - * @param {jqXHR} xhr jQuery Ajax object - * @param {String} status Description of response status - * @param {String} error_msg HTTP error message - */ - Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) { - this.events.trigger('checkpoint_restore_failed.Notebook'); - }; - /** * Delete a notebook checkpoint. * @@ -2647,18 +2468,13 @@ define([ */ Notebook.prototype.delete_checkpoint = function (checkpoint) { this.events.trigger('notebook_restoring.Notebook', checkpoint); - var url = utils.url_join_encode( - this.base_url, - 'api/contents', - this.notebook_path, - this.notebook_name, - 'checkpoints', - checkpoint - ); - $.ajax(url, { - type: 'DELETE', + var that = this; + this.contents.delete_checkpoint(this.notebook_path, this.notebook_name, + checkpoint, { success: $.proxy(this.delete_checkpoint_success, this), - error: $.proxy(this.delete_checkpoint_error, this) + error: function (error) { + that.events.trigger('checkpoint_delete_failed.Notebook', error); + } }); }; @@ -2666,27 +2482,12 @@ define([ * Success callback for deleting a notebook checkpoint * * @method delete_checkpoint_success - * @param {Object} data (ignored, should be empty) - * @param {String} status Description of response status - * @param {jqXHR} xhr jQuery Ajax object */ - Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) { - this.events.trigger('checkpoint_deleted.Notebook', data); + Notebook.prototype.delete_checkpoint_success = function () { + this.events.trigger('checkpoint_deleted.Notebook'); this.load_notebook(this.notebook_name, this.notebook_path); }; - /** - * Failure callback for deleting a notebook checkpoint. - * - * @method delete_checkpoint_error - * @param {jqXHR} xhr jQuery Ajax object - * @param {String} status Description of response status - * @param {String} error HTTP error message - */ - Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) { - this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]); - }; - // For backwards compatability. IPython.Notebook = Notebook; diff --git a/IPython/html/static/services/contents.js b/IPython/html/static/services/contents.js new file mode 100644 index 000000000..3cc5f11e5 --- /dev/null +++ b/IPython/html/static/services/contents.js @@ -0,0 +1,285 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'base/js/namespace', + 'jquery', + 'base/js/utils', + 'base/js/dialog', +], function(IPython, $, utils, dialog) { + var Contents = function(options) { + // Constructor + // + // A contents handles passing file operations + // to the back-end. This includes checkpointing + // with the normal file operations. + // + // Parameters: + // options: dictionary + // Dictionary of keyword arguments. + // base_url: string + this.base_url = options.base_url; + }; + + /** Error type */ + Contents.DIRECTORY_NOT_EMPTY_ERROR = 'DirectoryNotEmptyError'; + + Contents.DirectoryNotEmptyError = function() { + // Constructor + // + // An error representing the result of attempting to delete a non-empty + // directory. + this.message = 'A directory must be empty before being deleted.'; + } + Contents.DirectoryNotEmptyError.prototype = new Error; + Contents.DirectoryNotEmptyError.prototype.name = + Contents.DIRECTORY_NOT_EMPTY_ERROR; + + + Contents.prototype.api_url = function() { + var url_parts = [this.base_url, 'api/contents'].concat( + Array.prototype.slice.apply(arguments)); + return utils.url_join_encode.apply(null, url_parts); + }; + + /** + * Creates a basic error handler that wraps a jqXHR error as an Error. + * + * Takes a callback that accepts an Error, and returns a callback that can + * be passed directly to $.ajax, which will wrap the error from jQuery + * as an Error, and pass that to the original callback. + * + * @method create_basic_error_handler + * @param{Function} callback + * @return{Function} + */ + Contents.prototype.create_basic_error_handler = function(callback) { + if (!callback) { + return function(xhr, status, error) { }; + } + return function(xhr, status, error) { + callback(utils.wrap_ajax_error(xhr, status, error)); + }; + } + + /** + * File Functions (including notebook operations) + */ + + /** + * Load a file. + * + * Calls success with file JSON model, or error with error. + * + * @method load_notebook + * @param {String} path + * @param {String} name + * @param {Function} success + * @param {Function} error + */ + Contents.prototype.load = function (path, name, options) { + // We do the call with settings so we can set cache to false. + var settings = { + processData : false, + cache : false, + type : "GET", + dataType : "json", + success : options.success, + error : this.create_basic_error_handler(options.error) + }; + var url = this.api_url(path, name); + $.ajax(url, settings); + }; + + + /** + * Creates a new notebook file at the specified directory path. + * + * @method scroll_to_cell + * @param {String} path The path to create the new notebook at + * @param {String} name Name for new file. Chosen by server if unspecified. + * @param {Object} options: + * ext: file extension to use if name unspecified + */ + Contents.prototype.new = function(path, name, options) { + var method, data; + if (name) { + method = "PUT"; + } else { + method = "POST"; + data = JSON.stringify({ext: options.ext || ".ipynb"}); + } + + var settings = { + processData : false, + type : method, + data: data, + dataType : "json", + success : options.success || function() {}, + error : this.create_basic_error_handler(options.error) + }; + if (options.extra_settings) { + $.extend(settings, options.extra_settings); + } + $.ajax(this.api_url(path), settings); + }; + + Contents.prototype.delete = function(name, path, options) { + var error_callback = options.error || function() {}; + var that = this; + var settings = { + processData : false, + type : "DELETE", + dataType : "json", + success : options.success || function() {}, + error : function(xhr, status, error) { + // TODO: update IPEP27 to specify errors more precisely, so + // that error types can be detected here with certainty. + if (xhr.status === 400) { + error_callback(new Contents.DirectoryNotEmptyError()); + } + error_callback(utils.wrap_ajax_error(xhr, status, error)); + } + }; + var url = this.api_url(path, name); + $.ajax(url, settings); + }; + + Contents.prototype.rename = function(path, name, new_path, new_name, options) { + var data = {name: new_name, path: new_path}; + var settings = { + processData : false, + type : "PATCH", + data : JSON.stringify(data), + dataType: "json", + contentType: 'application/json', + success : options.success || function() {}, + error : this.create_basic_error_handler(options.error) + }; + var url = this.api_url(path, name); + $.ajax(url, settings); + }; + + Contents.prototype.save = function(path, name, model, options) { + // We do the call with settings so we can set cache to false. + var settings = { + processData : false, + type : "PUT", + data : JSON.stringify(model), + contentType: 'application/json', + success : options.success || function() {}, + error : this.create_basic_error_handler(options.error) + }; + if (options.extra_settings) { + $.extend(settings, options.extra_settings); + } + var url = this.api_url(path, name); + $.ajax(url, settings); + }; + + Contents.prototype.copy = function(to_path, to_name, from, options) { + var url, method; + if (to_name) { + url = this.api_url(to_path, to_name); + method = "PUT"; + } else { + url = this.api_url(to_path); + method = "POST"; + } + + var settings = { + processData : false, + type: method, + data: JSON.stringify({copy_from: from}), + dataType : "json", + success: options.success || function() {}, + error: this.create_basic_error_handler(options.error) + }; + if (options.extra_settings) { + $.extend(settings, options.extra_settings); + } + $.ajax(url, settings); + }; + + /** + * Checkpointing Functions + */ + + Contents.prototype.create_checkpoint = function(path, name, options) { + var url = this.api_url(path, name, 'checkpoints'); + var settings = { + type : "POST", + success: options.success || function() {}, + error : this.create_basic_error_handler(options.error) + }; + $.ajax(url, settings); + }; + + Contents.prototype.list_checkpoints = function(path, name, options) { + var url = this.api_url(path, name, 'checkpoints'); + var settings = { + type : "GET", + success: options.success, + error : this.create_basic_error_handler(options.error) + }; + $.ajax(url, settings); + }; + + Contents.prototype.restore_checkpoint = function(path, name, checkpoint_id, options) { + var url = this.api_url(path, name, 'checkpoints', checkpoint_id); + var settings = { + type : "POST", + success: options.success || function() {}, + error : this.create_basic_error_handler(options.error) + }; + $.ajax(url, settings); + }; + + Contents.prototype.delete_checkpoint = function(path, name, checkpoint_id, options) { + var url = this.api_url(path, name, 'checkpoints', checkpoint_id); + var settings = { + type : "DELETE", + success: options.success || function() {}, + error : this.create_basic_error_handler(options.error) + }; + $.ajax(url, settings); + }; + + /** + * File management functions + */ + + /** + * List notebooks and directories at a given path + * + * On success, load_callback is called with an array of dictionaries + * representing individual files or directories. Each dictionary has + * the keys: + * type: "notebook" or "directory" + * name: the name of the file or directory + * created: created date + * last_modified: last modified dat + * path: the path + * @method list_notebooks + * @param {String} path The path to list notebooks in + * @param {Function} load_callback called with list of notebooks on success + * @param {Function} error called with ajax results on error + */ + Contents.prototype.list_contents = function(path, options) { + var settings = { + processData : false, + cache : false, + type : "GET", + dataType : "json", + success : options.success, + error : this.create_basic_error_handler(options.error) + }; + + $.ajax(this.api_url(path), settings); + }; + + + IPython.Contents = Contents; + + return {'Contents': Contents}; +}); diff --git a/IPython/html/static/tree/js/main.js b/IPython/html/static/tree/js/main.js index 1f1975828..2396ad8db 100644 --- a/IPython/html/static/tree/js/main.js +++ b/IPython/html/static/tree/js/main.js @@ -7,6 +7,7 @@ require([ 'base/js/events', 'base/js/page', 'base/js/utils', + 'contents', 'tree/js/notebooklist', 'tree/js/clusterlist', 'tree/js/sessionlist', @@ -23,6 +24,7 @@ require([ events, page, utils, + contents, notebooklist, clusterlist, sesssionlist, @@ -39,7 +41,11 @@ require([ session_list = new sesssionlist.SesssionList($.extend({ events: events}, common_options)); + contents = new contents.Contents($.extend({ + events: events}, + common_options)); notebook_list = new notebooklist.NotebookList('#notebook_list', $.extend({ + contents: contents, session_list: session_list}, common_options)); cluster_list = new clusterlist.ClusterList('#cluster_list', common_options); @@ -54,7 +60,24 @@ require([ login_widget = new loginwidget.LoginWidget('#login_widget', common_options); $('#new_notebook').click(function (e) { - notebook_list.new_notebook(); + contents.new(common_options.notebook_path, null, { + ext: ".ipynb", + extra_settings: {async: false}, // So we can open a new window afterwards + success: function (data) { + window.open( + utils.url_join_encode( + common_options.base_url, 'notebooks', + data.path, data.name + ), '_blank'); + }, + error: function(error) { + dialog.modal({ + title : 'Creating Notebook Failed', + body : "The error was: " + error.message, + buttons : {'OK' : {'class' : 'btn-primary'}} + }); + } + }); }); var interval_id=0; @@ -118,5 +141,4 @@ require([ if (window.location.hash) { $("#tabs").find("a[href=" + window.location.hash + "]").click(); } - }); diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index 7f526d788..b36116720 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -20,6 +20,7 @@ define([ // element_name: string // base_url: string // notebook_path: string + // contents: Contents instance var that = this; this.session_list = options.session_list; // allow code re-use by just changing element_name in kernellist.js @@ -34,6 +35,7 @@ define([ this.sessions = {}; this.base_url = options.base_url || utils.get_body_data("baseUrl"); this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath"); + this.contents = options.contents; if (this.session_list && this.session_list.events) { this.session_list.events.on('sessions_loaded.Dashboard', function(e, d) { that.sessions_loaded(d); }); @@ -139,38 +141,27 @@ define([ }; NotebookList.prototype.load_list = function () { - var that = this; - var settings = { - processData : false, - cache : false, - type : "GET", - dataType : "json", - success : $.proxy(this.list_loaded, this), - error : $.proxy( function(xhr, status, error){ - utils.log_ajax_error(xhr, status, error); - that.list_loaded([], null, null, {msg:"Error connecting to server."}); - },this) - }; - - var url = utils.url_join_encode( - this.base_url, - 'api', - 'contents', - this.notebook_path - ); - $.ajax(url, settings); + var that = this + this.contents.list_contents(that.notebook_path, { + success: $.proxy(this.draw_notebook_list, this), + error: function(error) { + that.draw_notebook_list({content: []}, "Server error: " + error.message); + } + }); }; - - NotebookList.prototype.list_loaded = function (data, status, xhr, param) { - var message = 'Notebook list empty.'; - if (param !== undefined && param.msg) { - message = param.msg; - } + /** + * Draw the list of notebooks + * @method draw_notebook_list + * @param {Array} list An array of dictionaries representing files or + * directories. + * @param {String} error_msg An error message + */ + NotebookList.prototype.draw_notebook_list = function (list, error_msg) { + var message = error_msg || 'Notebook list empty.'; var item = null; var model = null; - var list = data.content; - var len = list.length; + var len = list.content.length; this.clear_list(); var n_uploads = this.element.children('.list_item').length; if (len === 0) { @@ -192,7 +183,7 @@ define([ offset += 1; } for (var i=0; i