Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a copy capability to jupyter notebooks #3071

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions notebook/services/contents/checkpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Checkpoints(LoggingConfigurable):

create_checkpoint(self, contents_mgr, path)
restore_checkpoint(self, contents_mgr, checkpoint_id, path)
rename_checkpoint(self, checkpoint_id, old_path, new_path)
rename_checkpoint(self, checkpoint_id, old_path, new_path, keep_old)
delete_checkpoint(self, checkpoint_id, path)
list_checkpoints(self, path)
"""
Expand All @@ -30,7 +30,7 @@ def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
"""Restore a checkpoint"""
raise NotImplementedError("must be implemented in a subclass")

def rename_checkpoint(self, checkpoint_id, old_path, new_path):
def rename_checkpoint(self, checkpoint_id, old_path, new_path, keep_old):
"""Rename a single checkpoint from old_path to new_path."""
raise NotImplementedError("must be implemented in a subclass")

Expand All @@ -42,10 +42,10 @@ def list_checkpoints(self, path):
"""Return a list of checkpoints for a given file"""
raise NotImplementedError("must be implemented in a subclass")

def rename_all_checkpoints(self, old_path, new_path):
def rename_all_checkpoints(self, old_path, new_path, keep_old):
"""Rename all checkpoints for old_path to new_path."""
for cp in self.list_checkpoints(old_path):
self.rename_checkpoint(cp['id'], old_path, new_path)
self.rename_checkpoint(cp['id'], old_path, new_path, keep_old)

def delete_all_checkpoints(self, path):
"""Delete all checkpoints for the given path."""
Expand All @@ -72,7 +72,7 @@ class GenericCheckpointsMixin(object):

- delete_checkpoint(self, checkpoint_id, path)
- list_checkpoints(self, path)
- rename_checkpoint(self, checkpoint_id, old_path, new_path)
- rename_checkpoint(self, checkpoint_id, old_path, new_path, keep_old)
"""

def create_checkpoint(self, contents_mgr, path):
Expand Down
7 changes: 5 additions & 2 deletions notebook/services/contents/filecheckpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def restore_checkpoint(self, contents_mgr, checkpoint_id, path):
self._copy(src_path, dest_path)

# ContentsManager-independent checkpoint API
def rename_checkpoint(self, checkpoint_id, old_path, new_path):
def rename_checkpoint(self, checkpoint_id, old_path, new_path, keep_old):
"""Rename a checkpoint from old_path to new_path."""
old_cp_path = self.checkpoint_path(checkpoint_id, old_path)
new_cp_path = self.checkpoint_path(checkpoint_id, new_path)
Expand All @@ -74,7 +74,10 @@ def rename_checkpoint(self, checkpoint_id, old_path, new_path):
new_cp_path,
)
with self.perm_to_403():
shutil.move(old_cp_path, new_cp_path)
if keep_old:
shutil.copy(old_cp_path, new_cp_path)
else:
shutil.move(old_cp_path, new_cp_path)

def delete_checkpoint(self, checkpoint_id, path):
"""delete a file's checkpoint"""
Expand Down
7 changes: 5 additions & 2 deletions notebook/services/contents/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ def delete_file(self, path):
with self.perm_to_403():
rm(os_path)

def rename_file(self, old_path, new_path):
def rename_file(self, old_path, new_path, keep_old):
"""Rename a file."""
old_path = old_path.strip('/')
new_path = new_path.strip('/')
Expand All @@ -535,7 +535,10 @@ def rename_file(self, old_path, new_path):
# Move the file
try:
with self.perm_to_403():
shutil.move(old_os_path, new_os_path)
if keep_old:
shutil.copy(old_os_path, new_os_path)
else:
shutil.move(old_os_path, new_os_path)
except web.HTTPError:
raise
except Exception as e:
Expand Down
11 changes: 6 additions & 5 deletions notebook/services/contents/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def delete_file(self, path):
"""Delete the file or directory at path."""
raise NotImplementedError('must be implemented in a subclass')

def rename_file(self, old_path, new_path):
def rename_file(self, old_path, new_path, keep_old):
"""Rename a file or directory."""
raise NotImplementedError('must be implemented in a subclass')

Expand All @@ -275,10 +275,10 @@ def delete(self, path):
self.delete_file(path)
self.checkpoints.delete_all_checkpoints(path)

def rename(self, old_path, new_path):
def rename(self, old_path, new_path, keep_old):
"""Rename a file and any checkpoints associated with that file."""
self.rename_file(old_path, new_path)
self.checkpoints.rename_all_checkpoints(old_path, new_path)
self.rename_file(old_path, new_path, keep_old)
self.checkpoints.rename_all_checkpoints(old_path, new_path, keep_old)

def update(self, model, path):
"""Update the file's path
Expand All @@ -288,8 +288,9 @@ def update(self, model, path):
"""
path = path.strip('/')
new_path = model.get('path', path).strip('/')
keep_old = (model.get('keep') == 'True')
if path != new_path:
self.rename(path, new_path)
self.rename(path, new_path, keep_old)
model = self.get(new_path, content=False)
return model

Expand Down
2 changes: 1 addition & 1 deletion notebook/static/base/js/namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ define(function(){
// tree
jglobal('SessionList','tree/js/sessionlist');

Jupyter.version = "5.2.1";
Jupyter.version = "5.3.0.dev0";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agrippa I think version changes automatically when start developing the code base. And should be changed when we are making a PR, if I'm not mistaken.
I'm currently new to the codebase so I might be wrong in this. 😄

Jupyter._target = '_blank';
return Jupyter;
});
Expand Down
4 changes: 2 additions & 2 deletions notebook/static/services/contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ define(function(require) {
);
};

Contents.prototype.rename = function(path, new_path) {
var data = {path: new_path};
Contents.prototype.rename = function(path, new_path, keep_old) {
var data = {path: new_path, keep: (keep_old ? 'True' : 'False')};
var settings = {
processData : false,
type : "PATCH",
Expand Down
34 changes: 22 additions & 12 deletions notebook/static/tree/js/notebooklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ define([
// Bind events for action buttons.
$('.rename-button').click($.proxy(this.rename_selected, this));
$('.move-button').click($.proxy(this.move_selected, this));
$('.copy-button').click($.proxy(this.copy_selected, this));
$('.download-button').click($.proxy(this.download_selected, this));
$('.shutdown-button').click($.proxy(this.shutdown_selected, this));
$('.duplicate-button').click($.proxy(this.duplicate_selected, this));
Expand Down Expand Up @@ -619,8 +620,10 @@ define([
// are a running notebook.
if (selected.length > 0 && !has_running_notebook) {
$('.move-button').css('display', 'inline-block');
$('.copy-button').css('display', 'inline-block');
} else {
$('.move-button').css('display', 'none');
$('.copy-button').css('display', 'none');
}

// Download is only visible when one item is selected, and it is not a
Expand Down Expand Up @@ -918,8 +921,7 @@ define([
});
};

NotebookList.prototype.move_selected = function() {
var that = this;
function move_or_copy_selected(op_type, that) {
var selected = that.selected.slice(); // Don't let that.selected change out from under us
var num_items = selected.length;

Expand All @@ -945,13 +947,9 @@ define([
input.addClass("path-input")
).addClass("move-path")
);
var d = dialog.modal({
title : i18n.msg.sprintf(i18n.msg.ngettext("Move an Item","Move %d Items",num_items),num_items),
body : dialog_body,
default_button: "Cancel",
buttons : {
Cancel : {},
Move : {
var dialog_buttons = {};
dialog_buttons['Cancel'] = {}
dialog_buttons[op_type] = {
class: "btn-primary",
click: function() {
// Move all the items.
Expand All @@ -960,14 +958,14 @@ define([
var item_name = item.name;
// Construct the new path using the user input and the item's name.
var new_path = utils.url_path_join(input.val(), item_name);
that.contents.rename(item_path, new_path).then(function() {
that.contents.rename(item_path, new_path, op_type === 'Copy').then(function() {
// After each move finishes, reload the list.
that.load_list();
}).catch(function(e) {
// If any of the moves fails, show this dialog for that move.
var failmsg = i18n.msg._("An error occurred while moving \"%1$s\" from \"%2$s\" to \"%3$s\".");
dialog.modal({
title: i18n.msg._("Move Failed"),
title: i18n.msg._(op_type + " Failed"),
body: $('<div/>')
.text(i18n.msg.sprintf(failmsg,item_name,item_path,new_path))
.append($('<div/>')
Expand All @@ -982,7 +980,11 @@ define([
}); // End of forEach.
}
}
},
var d = dialog.modal({
title : i18n.msg.sprintf(i18n.msg.ngettext(op_type + " an Item", op_type + " %d Items",num_items),num_items),
body : dialog_body,
default_button: "Cancel",
buttons : dialog_buttons,
// TODO: Consider adding fancier UI per Issue #941.
open : function () {
// Upon ENTER, click the OK button.
Expand All @@ -996,6 +998,14 @@ define([
input.focus();
}
});
}

NotebookList.prototype.move_selected = function() {
move_or_copy_selected('Move', this);
};

NotebookList.prototype.copy_selected = function() {
move_or_copy_selected('Copy', this);
};

NotebookList.prototype.download_selected = function() {
Expand Down
4 changes: 4 additions & 0 deletions notebook/static/tree/less/tree.less
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ ul#new-menu {
display: none;
}

.copy-button {
display: none;
}

.download-button {
display: none;
}
Expand Down
1 change: 1 addition & 0 deletions notebook/templates/tree.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<button title="{% trans %}Duplicate selected{% endtrans %}" class="duplicate-button btn btn-default btn-xs">{% trans %}Duplicate{% endtrans %}</button>
<button title="{% trans %}Rename selected{% endtrans %}" class="rename-button btn btn-default btn-xs">{% trans %}Rename{% endtrans %}</button>
<button title="{% trans %}Move selected{% endtrans %}" class="move-button btn btn-default btn-xs">{% trans %}Move{% endtrans %}</button>
<button title="{% trans %}Copy selected{% endtrans %}" class="copy-button btn btn-default btn-xs">{% trans %}Copy{% endtrans %}</button>
<button title="{% trans %}Download selected{% endtrans %}" class="download-button btn btn-default btn-xs">{% trans %}Download{% endtrans %}</button>
<button title="{% trans %}Shutdown selected notebook(s){% endtrans %}" class="shutdown-button btn btn-default btn-xs btn-warning">{% trans %}Shutdown{% endtrans %}</button>
<button title="{% trans %}View selected{% endtrans %}" class="view-button btn btn-default btn-xs">{% trans %}View{% endtrans %}</button>
Expand Down