diff --git a/IPython/html/static/notebook/js/cell.js b/IPython/html/static/notebook/js/cell.js index e695b3e..e0a8165 100644 --- a/IPython/html/static/notebook/js/cell.js +++ b/IPython/html/static/notebook/js/cell.js @@ -385,7 +385,8 @@ define([ **/ Cell.prototype.toJSON = function () { var data = {}; - data.metadata = this.metadata; + // deepcopy the metadata so copied cells don't share the same object + data.metadata = JSON.parse(JSON.stringify(this.metadata)); data.cell_type = this.cell_type; return data; }; @@ -404,22 +405,35 @@ define([ /** - * can the cell be split into two cells + * can the cell be split into two cells (false if not deletable) * @method is_splittable **/ Cell.prototype.is_splittable = function () { - return true; + return this.is_deletable(); }; /** - * can the cell be merged with other cells + * can the cell be merged with other cells (false if not deletable) * @method is_mergeable **/ Cell.prototype.is_mergeable = function () { - return true; + return this.is_deletable(); }; + /** + * is the cell deletable? only false (undeletable) if + * metadata.deletable is explicitly false -- everything else + * counts as true + * + * @method is_deletable + **/ + Cell.prototype.is_deletable = function () { + if (this.metadata.deletable === false) { + return false; + } + return true; + }; /** * @return {String} - the text before the cursor diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 1f65924..fa1b9ab 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -760,7 +760,11 @@ define([ */ Notebook.prototype.delete_cell = function (index) { var i = this.index_or_selected(index); - var cell = this.get_selected_cell(); + var cell = this.get_cell(i); + if (!cell.is_deletable()) { + return this; + } + this.undelete_backup = cell.toJSON(); $('#undelete_cell').removeClass('disabled'); if (this.is_valid_cell_index(i)) { @@ -1188,6 +1192,10 @@ define([ Notebook.prototype.copy_cell = function () { var cell = this.get_selected_cell(); this.clipboard = cell.toJSON(); + // remove undeletable status from the copied cell + if (this.clipboard.metadata.deletable !== undefined) { + delete this.clipboard.metadata.deletable; + } this.enable_paste(); }; diff --git a/IPython/html/tests/notebook/deletecell.js b/IPython/html/tests/notebook/deletecell.js new file mode 100644 index 0000000..6c98f28 --- /dev/null +++ b/IPython/html/tests/notebook/deletecell.js @@ -0,0 +1,107 @@ + +// Test +casper.notebook_test(function () { + var that = this; + var cell_is_deletable = function (index) { + // Get the deletable status of a cell. + return that.evaluate(function (index) { + var cell = IPython.notebook.get_cell(index); + return cell.is_deletable(); + }, index); + }; + + var a = 'print("a")'; + var index = this.append_cell(a); + + var b = 'print("b")'; + index = this.append_cell(b); + + var c = 'print("c")'; + index = this.append_cell(c); + + this.thenEvaluate(function() { + IPython.notebook.get_cell(1).metadata.deletable = false; + IPython.notebook.get_cell(2).metadata.deletable = 0; // deletable only when exactly false + IPython.notebook.get_cell(3).metadata.deletable = true; + }); + + this.then(function () { + // Check deletable status of the cells + this.test.assert(cell_is_deletable(0), 'Cell 0 is deletable'); + this.test.assert(!cell_is_deletable(1), 'Cell 1 is not deletable'); + this.test.assert(cell_is_deletable(2), 'Cell 2 is deletable'); + this.test.assert(cell_is_deletable(3), 'Cell 3 is deletable'); + }); + + // Try to delete cell 0 (should succeed) + this.then(function () { + this.select_cell(0); + this.trigger_keydown('esc'); + this.trigger_keydown('d', 'd'); + this.test.assertEquals(this.get_cells_length(), 3, 'Delete cell 0: There are now 3 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 0: Cell 1 is now cell 0'); + this.test.assertEquals(this.get_cell_text(1), b, 'Delete cell 0: Cell 2 is now cell 1'); + this.test.assertEquals(this.get_cell_text(2), c, 'Delete cell 0: Cell 3 is now cell 2'); + this.validate_notebook_state('dd', 'command', 0); + }); + + // Try to delete cell 0 (should fail) + this.then(function () { + this.select_cell(0); + this.trigger_keydown('d', 'd'); + this.test.assertEquals(this.get_cells_length(), 3, 'Delete cell 0: There are still 3 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 0: Cell 0 was not deleted'); + this.test.assertEquals(this.get_cell_text(1), b, 'Delete cell 0: Cell 1 was not affected'); + this.test.assertEquals(this.get_cell_text(2), c, 'Delete cell 0: Cell 2 was not affected'); + this.validate_notebook_state('dd', 'command', 0); + }); + + // Try to delete cell 1 (should succeed) + this.then(function () { + this.select_cell(1); + this.trigger_keydown('d', 'd'); + this.test.assertEquals(this.get_cells_length(), 2, 'Delete cell 1: There are now 2 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 1: Cell 0 was not affected'); + this.test.assertEquals(this.get_cell_text(1), c, 'Delete cell 1: Cell 1 was not affected'); + this.validate_notebook_state('dd', 'command', 1); + }); + + // Try to delete cell 1 (should succeed) + this.then(function () { + this.select_cell(1); + this.trigger_keydown('d', 'd'); + this.test.assertEquals(this.get_cells_length(), 1, 'Delete cell 1: There is now 1 cell'); + this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 2: Cell 0 was not affected'); + this.validate_notebook_state('dd', 'command', 0); + }); + + // Change the deletable status of the last cells + this.thenEvaluate(function() { + IPython.notebook.get_cell(0).metadata.deletable = true; + }); + + this.then(function () { + // Check deletable status of the cell + this.test.assert(cell_is_deletable(0), 'Cell 0 is deletable'); + + // Try to delete the last cell (should succeed) + this.select_cell(0); + this.trigger_keydown('d', 'd'); + this.test.assertEquals(this.get_cells_length(), 1, 'Delete last cell: There is still 1 cell'); + this.test.assertEquals(this.get_cell_text(0), "", 'Delete last cell: Cell 0 was deleted'); + this.validate_notebook_state('dd', 'command', 0); + }); + + // Make sure copied cells are deletable + this.thenEvaluate(function() { + IPython.notebook.get_cell(0).metadata.deletable = false; + }); + this.then(function () { + this.select_cell(0); + this.trigger_keydown('c', 'v'); + this.test.assertEquals(this.get_cells_length(), 2, 'Copy cell: There are 2 cells'); + this.test.assert(!cell_is_deletable(0), 'Cell 0 is not deletable'); + this.test.assert(cell_is_deletable(1), 'Cell 1 is deletable'); + this.validate_notebook_state('cv', 'command', 1); + }); +}); diff --git a/IPython/html/tests/notebook/dualmode_merge.js b/IPython/html/tests/notebook/dualmode_merge.js index 573b457..8ec3240 100644 --- a/IPython/html/tests/notebook/dualmode_merge.js +++ b/IPython/html/tests/notebook/dualmode_merge.js @@ -1,6 +1,33 @@ // Test casper.notebook_test(function () { + var a = 'ab\ncd'; + var b = 'print("b")'; + var c = 'print("c")'; + + var that = this; + var cell_is_mergeable = function (index) { + // Get the mergeable status of a cell. + return that.evaluate(function (index) { + var cell = IPython.notebook.get_cell(index); + return cell.is_mergeable(); + }, index); + }; + + var cell_is_splittable = function (index) { + // Get the splittable status of a cell. + return that.evaluate(function (index) { + var cell = IPython.notebook.get_cell(index); + return cell.is_splittable(); + }, index); + }; + + var close_dialog = function () { + this.evaluate(function(){ + $('div.modal-footer button.btn-default').click(); + }, {}); + }; + this.then(function () { // Split and merge cells this.select_cell(0); @@ -16,6 +43,93 @@ casper.notebook_test(function () { this.select_cell(0); // Move up to cell 0 this.trigger_keydown('shift-m'); // Merge this.validate_notebook_state('merge', 'command', 0); - this.test.assertEquals(this.get_cell_text(0), 'ab\ncd', 'merge; Verify that cell 0 has the merged contents.'); + this.test.assertEquals(this.get_cell_text(0), a, 'merge; Verify that cell 0 has the merged contents.'); + }); + + // add some more cells and test splitting/merging when a cell is not deletable + this.then(function () { + this.append_cell(b); + this.append_cell(c); + }); + + this.thenEvaluate(function() { + IPython.notebook.get_cell(1).metadata.deletable = false; + }); + + // Check that merge/split status are correct + this.then(function () { + this.test.assert(cell_is_splittable(0), 'Cell 0 is splittable'); + this.test.assert(cell_is_mergeable(0), 'Cell 0 is mergeable'); + this.test.assert(!cell_is_splittable(1), 'Cell 1 is not splittable'); + this.test.assert(!cell_is_mergeable(1), 'Cell 1 is not mergeable'); + this.test.assert(cell_is_splittable(2), 'Cell 2 is splittable'); + this.test.assert(cell_is_mergeable(2), 'Cell 2 is mergeable'); + }); + + // Try to merge cell 0 below with cell 1 + this.then(function () { + this.select_cell(0); + this.trigger_keydown('esc'); + this.trigger_keydown('shift-m'); + this.test.assertEquals(this.get_cells_length(), 3, 'Merge cell 0 down: There are still 3 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 0 down: Cell 0 is unchanged'); + this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 0 down: Cell 1 is unchanged'); + this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 0 down: Cell 2 is unchanged'); + this.validate_notebook_state('shift-m', 'command', 0); + }); + + // Try to merge cell 1 above with cell 0 + this.then(function () { + this.select_cell(1); + }); + this.thenEvaluate(function () { + IPython.notebook.merge_cell_above(); + }); + this.then(function () { + this.test.assertEquals(this.get_cells_length(), 3, 'Merge cell 1 up: There are still 3 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 1 up: Cell 0 is unchanged'); + this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 1 up: Cell 1 is unchanged'); + this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 1 up: Cell 2 is unchanged'); + this.validate_notebook_state('merge up', 'command', 1); + }); + + // Try to split cell 1 + this.then(function () { + this.select_cell(1); + this.trigger_keydown('enter'); + this.set_cell_editor_cursor(1, 0, 2); + this.trigger_keydown('ctrl-shift-subtract'); // Split + this.test.assertEquals(this.get_cells_length(), 3, 'Split cell 1: There are still 3 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Split cell 1: Cell 0 is unchanged'); + this.test.assertEquals(this.get_cell_text(1), b, 'Split cell 1: Cell 1 is unchanged'); + this.test.assertEquals(this.get_cell_text(2), c, 'Split cell 1: Cell 2 is unchanged'); + this.validate_notebook_state('ctrl-shift-subtract', 'edit', 1); + }); + + // Try to merge cell 1 down + this.then(function () { + this.select_cell(1); + this.trigger_keydown('esc'); + this.trigger_keydown('shift-m'); + this.test.assertEquals(this.get_cells_length(), 3, 'Merge cell 1 down: There are still 3 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 1 down: Cell 0 is unchanged'); + this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 1 down: Cell 1 is unchanged'); + this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 1 down: Cell 2 is unchanged'); + this.validate_notebook_state('shift-m', 'command', 1); + }); + + // Try to merge cell 2 above with cell 1 + this.then(function () { + this.select_cell(2); + }); + this.thenEvaluate(function () { + IPython.notebook.merge_cell_above(); + }); + this.then(function () { + this.test.assertEquals(this.get_cells_length(), 3, 'Merge cell 2 up: There are still 3 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Merge cell 2 up: Cell 0 is unchanged'); + this.test.assertEquals(this.get_cell_text(1), b, 'Merge cell 2 up: Cell 1 is unchanged'); + this.test.assertEquals(this.get_cell_text(2), c, 'Merge cell 2 up: Cell 2 is unchanged'); + this.validate_notebook_state('merge up', 'command', 2); }); -}); \ No newline at end of file +});