diff --git a/notebook/services/contents/handlers.py b/notebook/services/contents/handlers.py index 25dcaf213c9..c2515aee69d 100644 --- a/notebook/services/contents/handlers.py +++ b/notebook/services/contents/handlers.py @@ -195,7 +195,8 @@ def post(self, path=''): ext = model.get('ext', '') type = model.get('type', '') if copy_from: - yield self._copy(copy_from, path) + copy_to = model.get('copy_to') or path + yield self._copy(copy_from, copy_to) else: yield self._new_untitled(path, type=type, ext=ext) else: diff --git a/notebook/static/notebook/js/menubar.js b/notebook/static/notebook/js/menubar.js index ddefd0cd1f6..6f3b955ee16 100644 --- a/notebook/static/notebook/js/menubar.js +++ b/notebook/static/notebook/js/menubar.js @@ -170,6 +170,12 @@ define([ that.notebook.copy_notebook(); return false; }); + + this.element.find('#save_as').click(function() { + that.notebook.save_notebook_as(); + return false; + }) + this.element.find('#download_ipynb').click(function () { var base_url = that.notebook.base_url; var notebook_path = utils.encode_uri_components(that.notebook.notebook_path); diff --git a/notebook/static/notebook/js/notebook.js b/notebook/static/notebook/js/notebook.js index 39b17d2b233..f8874d32c68 100644 --- a/notebook/static/notebook/js/notebook.js +++ b/notebook/static/notebook/js/notebook.js @@ -2830,7 +2830,63 @@ define([ this._checkpoint_after_save = false; } }; - + + Notebook.prototype.save_notebook_as = function(new_name) { + var that = this; + var dialog_body = $('
').append( + $("

").addClass("save-message") + .text(i18n.msg._('Path must be relative to the notebook root directory')) + ).append( + $("
") + ).append( + $('').attr('type','text').attr('size','25') + .addClass('form-control').attr('placeholder', 'Enter notebook name here') + ); + var d = dialog.modal({ + title: 'Save As', + body: dialog_body, + keyboard_manager: this.keyboard_manager, + notebook: this, + buttons: { + Cancel: {}, + Save: { + class: 'btn-primary', + click: function() { + var nb_name_or_path = d.find('input').val(); + var nb_name = nb_name_or_path.split("/").slice(-1).pop(); + var ext = utils.splitext(nb_name)[1]; + if (ext && ext!== '.ipynb') { + d.find('.save-message').append('
').append(i18n.msg._( + "Notebook files should have extension .ipynb") + ); + } else if (!that.test_notebook_name(nb_name)) { + d.find('.save-message').append('
').append(i18n.msg._( + "Notebook names must have 1 or more characters and can contain any characters except :/\\") + ); + } + else if (ext === '') { + nb_name_or_path += '.ipynb'; + } + that.copy_notebook(nb_name_or_path); + return false; + } + }, + }, + open : function () { + /** + * Upon ENTER, click the OK button. + */ + d.find('input[type="text"]').keydown(function (event) { + if (event.which === keyboard.keycodes.enter) { + d.find('.btn-primary').first().click(); + return false; + } + }); + d.find('input[type="text"]').focus().select(); + } + }); + } + /** * Update the autosave interval based on the duration of the last save. * @@ -2911,11 +2967,12 @@ define([ * Make a copy of the current notebook. * If the notebook has unsaved changes, it is saved first. */ - Notebook.prototype.copy_notebook = function () { + Notebook.prototype.copy_notebook = function (new_name) { var that = this; var base_url = this.base_url; var w = window.open('', IPython._target); var parent = utils.url_path_split(this.notebook_path)[0]; + new_name = new_name || ''; var p; if (this.dirty && this.writable) { p = this.save_notebook(true); @@ -2923,7 +2980,7 @@ define([ p = Promise.resolve(); } return p.then(function () { - return that.contents.copy(that.notebook_path, parent).then( + return that.contents.copy(that.notebook_path, parent, new_name).then( function (data) { w.location = utils.url_path_join( base_url, 'notebooks', utils.encode_uri_components(data.path) diff --git a/notebook/static/services/contents.js b/notebook/static/services/contents.js index 0425959b3e0..fe56eb051e6 100644 --- a/notebook/static/services/contents.js +++ b/notebook/static/services/contents.js @@ -186,17 +186,23 @@ define(function(requirejs) { return utils.promising_ajax(url, settings); }; - Contents.prototype.copy = function(from_file, to_dir) { + Contents.prototype.copy = function(from_file, to_dir, name='') { /** * Copy a file into a given directory via POST - * The server will select the name of the copied file + * If name is not given, the server will select the name of the + * copied file */ var url = this.api_url(to_dir); + var body = {copy_from: from_file}; + + if (name) { + body.copy_to = name + } var settings = { processData : false, type: "POST", - data: JSON.stringify({copy_from: from_file}), + data: JSON.stringify(body), contentType: 'application/json', dataType : "json", }; diff --git a/notebook/templates/notebook.html b/notebook/templates/notebook.html index 647b92850a1..aff23447666 100644 --- a/notebook/templates/notebook.html +++ b/notebook/templates/notebook.html @@ -88,7 +88,10 @@

  • - {% trans %}Make a Copy...{% endtrans %}
  • + {% trans %}Make a Copy...{% endtrans %} +
  • + {% trans %}Save As{% endtrans %}
  • {% trans %}Rename...{% endtrans %}
  • {% trans %}Save and Checkpoint{% endtrans %}