diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 3cd81f563..81fda2dc2 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -73,6 +73,7 @@ from .services.sessions.sessionmanager import SessionManager from .base.handlers import AuthenticatedFileHandler, FileFindHandler +from IPython.config import Config from IPython.config.application import catch_config_error, boolean_flag from IPython.core.application import BaseIPythonApplication from IPython.core.profiledir import ProfileDir @@ -554,10 +555,12 @@ class NotebookApp(BaseIPythonApplication): # Use config here, to ensure that it takes higher priority than # anything that comes from the profile. + c = Config() if os.path.isdir(f): - self.config.NotebookApp.notebook_dir = f + c.NotebookApp.notebook_dir = f elif os.path.isfile(f): - self.config.NotebookApp.file_to_run = f + c.NotebookApp.file_to_run = f + self.update_config(c) def init_kernel_argv(self): """construct the kernel arguments""" diff --git a/IPython/html/static/base/js/keyboard.js b/IPython/html/static/base/js/keyboard.js index 4fa77d5c9..56391e6d6 100644 --- a/IPython/html/static/base/js/keyboard.js +++ b/IPython/html/static/base/js/keyboard.js @@ -128,15 +128,6 @@ IPython.keyboard = (function (IPython) { return shortcut; }; - var trigger_keydown = function (shortcut, element) { - // Trigger shortcut keydown on an element - element = element || document; - element = $(element); - var event = shortcut_to_event(shortcut, 'keydown'); - element.trigger(event); - }; - - // Shortcut manager class var ShortcutManager = function (delay) { @@ -252,7 +243,7 @@ IPython.keyboard = (function (IPython) { ShortcutManager.prototype.handles = function (event) { var shortcut = event_to_shortcut(event); var data = this._shortcuts[shortcut]; - return !( data === undefined ) + return !( data === undefined || data.handler === undefined ) } return { @@ -262,8 +253,7 @@ IPython.keyboard = (function (IPython) { normalize_key : normalize_key, normalize_shortcut : normalize_shortcut, shortcut_to_event : shortcut_to_event, - event_to_shortcut : event_to_shortcut, - trigger_keydown : trigger_keydown + event_to_shortcut : event_to_shortcut }; }(IPython)); diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 05cb955c1..e9f7a78de 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -58,6 +58,9 @@ var IPython = (function (IPython) { this.style(); this.create_elements(); this.bind_events(); + this.save_notebook = function() { // don't allow save until notebook_loaded + this.save_notebook_error(null, null, "Load failed, save is disabled"); + }; }; /** @@ -1723,7 +1726,8 @@ var IPython = (function (IPython) { }; /** - * Save this notebook on the server. + * Save this notebook on the server. This becomes a notebook instance's + * .save_notebook method *after* the entire notebook has been loaded. * * @method save_notebook */ @@ -1829,7 +1833,7 @@ var IPython = (function (IPython) { " Selecting trust will immediately reload this notebook in a trusted state." ).append( " For more information, see the " - ).append($("").attr("href", "http://ipython.org/security.html") + ).append($("").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html") .text("IPython security documentation") ).append(".") ); @@ -2100,7 +2104,9 @@ var IPython = (function (IPython) { IPython.CellToolbar.global_show(); IPython.CellToolbar.activate_preset(this.metadata.celltoolbar); } - + + // now that we're fully loaded, it is safe to restore save functionality + delete(this.save_notebook); $([IPython.events]).trigger('notebook_loaded.Notebook'); }; diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index 87f508ef1..a2ecad1dc 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -188,8 +188,8 @@ var IPython = (function (IPython) { $([IPython.events]).on('notebook_saved.Notebook', function () { nnw.set_message("Notebook saved",2000); }); - $([IPython.events]).on('notebook_save_failed.Notebook', function () { - nnw.set_message("Notebook save failed"); + $([IPython.events]).on('notebook_save_failed.Notebook', function (evt, xhr, status, data) { + nnw.set_message(data || "Notebook save failed"); }); // Checkpoint events diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index 47f239f3c..79b902e14 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -541,6 +541,10 @@ var IPython = (function (IPython) { var container = element; container.show = function(){console.log('Warning "container.show()" is deprecated.')}; // end backward compat + + // Fix for ipython/issues/5293, make sure `element` is the area which + // output can be inserted into at the time of JS execution. + element = toinsert; try { eval(js); } catch(err) { diff --git a/IPython/html/static/notebook/js/tooltip.js b/IPython/html/static/notebook/js/tooltip.js index 915cca4a0..af5cbe683 100644 --- a/IPython/html/static/notebook/js/tooltip.js +++ b/IPython/html/static/notebook/js/tooltip.js @@ -131,17 +131,13 @@ var IPython = (function (IPython) { Tooltip.prototype.showInPager = function (cell) { // reexecute last call in pager by appending ? to show back in pager var that = this; - var empty = function () {}; - cell.kernel.execute( - that.name + '?', { - 'execute_reply': empty, - 'output': empty, - 'clear_output': empty, - 'cell': cell - }, { - 'silent': false, - 'store_history': true - }); + var callbacks = {'shell' : { + 'payload' : { + 'page' : $.proxy(cell._open_with_pager, cell) + } + } + }; + cell.kernel.execute(that.name + '?', callbacks, {'silent': false, 'store_history': true}); this.remove_and_cancel_tooltip(); }; diff --git a/IPython/html/static/notebook/less/cell.less b/IPython/html/static/notebook/less/cell.less index 5514360d0..3cecbdf1b 100644 --- a/IPython/html/static/notebook/less/cell.less +++ b/IPython/html/static/notebook/less/cell.less @@ -33,6 +33,14 @@ div.prompt { line-height: @code_line_height; } +@media (max-width: 480px) { + // prompts are in the main column on small screens, + // so text should be left-aligned + div.prompt { + text-align: left; + } +} + div.inner_cell { .vbox(); .box-flex1(); diff --git a/IPython/html/static/notebook/less/codecell.less b/IPython/html/static/notebook/less/codecell.less index 777f18584..8fc60b537 100644 --- a/IPython/html/static/notebook/less/codecell.less +++ b/IPython/html/static/notebook/less/codecell.less @@ -10,13 +10,19 @@ div.input { .hbox(); } +@media (max-width: 480px) { + // move prompts above code on small screens + div.input { + .vbox(); + } +} + /* input_area and input_prompt must match in top border and margin for alignment */ div.input_prompt { color: navy; border-top: 1px solid transparent; } - // The styles related to div.highlight are for nbconvert HTML output only. This works // because the .highlight div isn't present in the live notebook. We could put this into // nbconvert, but it easily falls out of sync, can't use our less variables and doesn't diff --git a/IPython/html/static/notebook/less/notebook.less b/IPython/html/static/notebook/less/notebook.less index 8c6589e47..f7345d0a2 100644 --- a/IPython/html/static/notebook/less/notebook.less +++ b/IPython/html/static/notebook/less/notebook.less @@ -7,6 +7,14 @@ body.notebook_app { overflow: hidden; } +@media (max-width: 767px) { + // remove bootstrap-responsive's body padding on small screens + body.notebook_app { + padding-left: 0px; + padding-right: 0px; + } +} + span#notebook_name { height: 1em; line-height: 1em; diff --git a/IPython/html/static/notebook/less/outputarea.less b/IPython/html/static/notebook/less/outputarea.less index e129385a2..5dd1614fa 100644 --- a/IPython/html/static/notebook/less/outputarea.less +++ b/IPython/html/static/notebook/less/outputarea.less @@ -72,6 +72,13 @@ div.output_area { .vbox(); } +@media (max-width: 480px) { + // move prompts above output on small screens + div.output_area { + .vbox(); + } +} + div.output_area pre { margin: 0; padding: 0; diff --git a/IPython/html/static/notebook/less/textcell.less b/IPython/html/static/notebook/less/textcell.less index 0ec35bffc..7abe33924 100644 --- a/IPython/html/static/notebook/less/textcell.less +++ b/IPython/html/static/notebook/less/textcell.less @@ -2,6 +2,12 @@ div.text_cell { padding: 5px 5px 5px 0px; .hbox(); } +@media (max-width: 480px) { + // remove prompt indentation on small screens + div.text_cell > div.prompt { + display: none; + } +} div.text_cell_render { /*font-family: "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;*/ diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index b608f6945..06e5ec568 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -73,11 +73,11 @@ div.cell{border:1px solid transparent;display:-webkit-box;-webkit-box-orient:ver div.cell.edit_mode{border-radius:4px;border:thin #008000 solid} div.cell{width:100%;padding:5px 5px 5px 0;margin:0;outline:none} div.prompt{min-width:11ex;padding:.4em;margin:0;font-family:monospace;text-align:right;line-height:1.21429em} -div.inner_cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1} +@media (max-width:480px){div.prompt{text-align:left}}div.inner_cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1} div.input_area{border:1px solid #cfcfcf;border-radius:4px;background:#f7f7f7} div.prompt:empty{padding-top:0;padding-bottom:0} div.input{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch} -div.input_prompt{color:#000080;border-top:1px solid transparent} +@media (max-width:480px){div.input{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch}}div.input_prompt{color:#000080;border-top:1px solid transparent} div.input_area>div.highlight{margin:.4em;border:none;padding:0;background-color:transparent} div.input_area>div.highlight>pre{margin:0;border:0;padding:0;background-color:transparent;font-size:14px;line-height:1.21429em} .CodeMirror{line-height:1.21429em;height:auto;background:none;} @@ -117,7 +117,7 @@ div.output_area{padding:0;page-break-inside:avoid;display:-webkit-box;-webkit-bo div.output_area .rendered_html table{margin-left:0;margin-right:0} div.output_area .rendered_html img{margin-left:0;margin-right:0} .output{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch} -div.output_area pre{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline;color:#000;background-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;line-height:inherit} +@media (max-width:480px){div.output_area{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch}}div.output_area pre{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline;color:#000;background-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;line-height:inherit} div.output_subarea{padding:.4em .4em 0 .4em;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1} div.output_text{text-align:left;color:#000;line-height:1.21429em} div.output_stderr{background:#fdd;} @@ -170,7 +170,7 @@ p.p-space{margin-bottom:10px} .rendered_html img{display:block;margin-left:auto;margin-right:auto} .rendered_html *+img{margin-top:1em} div.text_cell{padding:5px 5px 5px 0;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch} -div.text_cell_render{outline:none;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000} +@media (max-width:480px){div.text_cell>div.prompt{display:none}}div.text_cell_render{outline:none;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000} a.anchor-link:link{text-decoration:none;padding:0 20px;visibility:hidden} h1:hover .anchor-link,h2:hover .anchor-link,h3:hover .anchor-link,h4:hover .anchor-link,h5:hover .anchor-link,h6:hover .anchor-link{visibility:visible} div.cell.text_cell.rendered{padding:0} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index 36563fd52..df65a85f0 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -1350,11 +1350,11 @@ div.cell{border:1px solid transparent;display:-webkit-box;-webkit-box-orient:ver div.cell.edit_mode{border-radius:4px;border:thin #008000 solid} div.cell{width:100%;padding:5px 5px 5px 0;margin:0;outline:none} div.prompt{min-width:11ex;padding:.4em;margin:0;font-family:monospace;text-align:right;line-height:1.21429em} -div.inner_cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1} +@media (max-width:480px){div.prompt{text-align:left}}div.inner_cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1} div.input_area{border:1px solid #cfcfcf;border-radius:4px;background:#f7f7f7} div.prompt:empty{padding-top:0;padding-bottom:0} div.input{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch} -div.input_prompt{color:#000080;border-top:1px solid transparent} +@media (max-width:480px){div.input{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch}}div.input_prompt{color:#000080;border-top:1px solid transparent} div.input_area>div.highlight{margin:.4em;border:none;padding:0;background-color:transparent} div.input_area>div.highlight>pre{margin:0;border:0;padding:0;background-color:transparent;font-size:14px;line-height:1.21429em} .CodeMirror{line-height:1.21429em;height:auto;background:none;} @@ -1394,7 +1394,7 @@ div.output_area{padding:0;page-break-inside:avoid;display:-webkit-box;-webkit-bo div.output_area .rendered_html table{margin-left:0;margin-right:0} div.output_area .rendered_html img{margin-left:0;margin-right:0} .output{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch} -div.output_area pre{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline;color:#000;background-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;line-height:inherit} +@media (max-width:480px){div.output_area{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;width:100%;display:flex;flex-direction:column;align-items:stretch}}div.output_area pre{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline;color:#000;background-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;line-height:inherit} div.output_subarea{padding:.4em .4em 0 .4em;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1} div.output_text{text-align:left;color:#000;line-height:1.21429em} div.output_stderr{background:#fdd;} @@ -1447,7 +1447,7 @@ p.p-space{margin-bottom:10px} .rendered_html img{display:block;margin-left:auto;margin-right:auto} .rendered_html *+img{margin-top:1em} div.text_cell{padding:5px 5px 5px 0;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch} -div.text_cell_render{outline:none;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000} +@media (max-width:480px){div.text_cell>div.prompt{display:none}}div.text_cell_render{outline:none;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000} a.anchor-link:link{text-decoration:none;padding:0 20px;visibility:hidden} h1:hover .anchor-link,h2:hover .anchor-link,h3:hover .anchor-link,h4:hover .anchor-link,h5:hover .anchor-link,h6:hover .anchor-link{visibility:visible} div.cell.text_cell.rendered{padding:0} @@ -1476,7 +1476,7 @@ div.cell.text_cell.rendered{padding:0} .docked-widget-modal{overflow:hidden;position:relative !important;top:0 !important;left:0 !important;margin-left:0 !important} body{background-color:#fff} body.notebook_app{overflow:hidden} -span#notebook_name{height:1em;line-height:1em;padding:3px;border:none;font-size:146.5%} +@media (max-width:767px){body.notebook_app{padding-left:0;padding-right:0}}span#notebook_name{height:1em;line-height:1em;padding:3px;border:none;font-size:146.5%} div#notebook_panel{margin:0 0 0 0;padding:0;-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)} div#notebook{font-size:14px;line-height:20px;overflow-y:scroll;overflow-x:auto;width:100%;padding:1em 0 1em 0;margin:0;border-top:1px solid #ababab;outline:none;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box} div.ui-widget-content{border:1px solid #ababab;outline:none} diff --git a/IPython/html/templates/notebook.html b/IPython/html/templates/notebook.html index 097a416f1..eebd652fd 100644 --- a/IPython/html/templates/notebook.html +++ b/IPython/html/templates/notebook.html @@ -227,14 +227,14 @@ class="notebook_app" ( ("http://ipython.org/documentation.html","IPython Help",True), ("http://nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/notebooks/", "Notebook Examples", True), - ("http://ipython.org/ipython-doc/stable/interactive/notebook.html","Notebook Help",True), - ("http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html","Editor Shortcuts",True), + ("http://ipython.org/ipython-doc/2/notebook/notebook.html","Notebook Help",True), + ("http://ipython.org/ipython-doc/2/notebook/cm_keyboard.html","Editor Shortcuts",True), ),( ("http://docs.python.org","Python",True), ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True), ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True), ("http://matplotlib.org/contents.html","Matplotlib",True), - ("http://docs.sympy.org/dev/index.html","SymPy",True), + ("http://docs.sympy.org/latest/index.html","SymPy",True), ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True) ) ) diff --git a/IPython/html/tests/notebook/dualmode.js b/IPython/html/tests/notebook/dualmode.js new file mode 100644 index 000000000..87b556765 --- /dev/null +++ b/IPython/html/tests/notebook/dualmode.js @@ -0,0 +1,78 @@ +// Test the notebook dual mode feature. + +// Test +casper.notebook_test(function () { + var a = 'print("a")'; + var index = this.append_cell(a); + this.execute_cell_then(index); + + var b = 'print("b")'; + index = this.append_cell(b); + this.execute_cell_then(index); + + var c = 'print("c")'; + index = this.append_cell(c); + this.execute_cell_then(index); + + this.then(function () { + this.validate_notebook_state('initial state', 'edit', 0); + this.trigger_keydown('esc'); + this.validate_notebook_state('esc', 'command', 0); + this.trigger_keydown('down'); + this.validate_notebook_state('down', 'command', 1); + this.trigger_keydown('enter'); + this.validate_notebook_state('enter', 'edit', 1); + this.trigger_keydown('j'); + this.validate_notebook_state('j in edit mode', 'edit', 1); + this.trigger_keydown('esc'); + this.validate_notebook_state('esc', 'command', 1); + this.trigger_keydown('j'); + this.validate_notebook_state('j in command mode', 'command', 2); + this.click_cell_editor(0); + this.validate_notebook_state('click cell 0', 'edit', 0); + this.click_cell_editor(3); + this.validate_notebook_state('click cell 3', 'edit', 3); + this.trigger_keydown('esc'); + this.validate_notebook_state('esc', 'command', 3); + + // Open keyboard help + this.evaluate(function(){ + $('#keyboard_shortcuts a').click(); + }, {}); + + this.trigger_keydown('k'); + this.validate_notebook_state('k in command mode while keyboard help is up', 'command', 3); + + // Close keyboard help + this.evaluate(function(){ + $('div.modal button.close').click(); + }, {}); + + this.trigger_keydown('k'); + this.validate_notebook_state('k in command mode', 'command', 2); + this.click_cell_editor(0); + this.validate_notebook_state('click cell 0', 'edit', 0); + this.focus_notebook(); + this.validate_notebook_state('focus #notebook', 'command', 0); + this.click_cell_editor(0); + this.validate_notebook_state('click cell 0', 'edit', 0); + this.focus_notebook(); + this.validate_notebook_state('focus #notebook', 'command', 0); + this.click_cell_editor(3); + this.validate_notebook_state('click cell 3', 'edit', 3); + + // Cell deletion + this.trigger_keydown('esc', 'd', 'd'); + this.test.assertEquals(this.get_cells_length(), 3, 'dd actually deletes a cell'); + this.validate_notebook_state('dd', 'command', 2); + + // Make sure that if the time between d presses is too long, nothing gets removed. + this.trigger_keydown('d'); + }); + this.wait(1000); + this.then(function () { + this.trigger_keydown('d'); + this.test.assertEquals(this.get_cells_length(), 3, "d, 1 second wait, d doesn't delete a cell"); + this.validate_notebook_state('d, 1 second wait, d', 'command', 2); + }); +}); diff --git a/IPython/html/tests/notebook/dualmode_arrows.js b/IPython/html/tests/notebook/dualmode_arrows.js new file mode 100644 index 000000000..034929b5f --- /dev/null +++ b/IPython/html/tests/notebook/dualmode_arrows.js @@ -0,0 +1,51 @@ + +// Test +casper.notebook_test(function () { + var a = 'print("a")'; + var index = this.append_cell(a); + this.execute_cell_then(index); + + var b = 'print("b")'; + index = this.append_cell(b); + this.execute_cell_then(index); + + var c = 'print("c")'; + index = this.append_cell(c); + this.execute_cell_then(index); + + this.then(function () { + + // Up and down in command mode + this.select_cell(3); + this.trigger_keydown('j'); + this.validate_notebook_state('j at end of notebook', 'command', 3); + this.trigger_keydown('down'); + this.validate_notebook_state('down at end of notebook', 'command', 3); + this.trigger_keydown('up'); + this.validate_notebook_state('up', 'command', 2); + this.select_cell(0); + this.validate_notebook_state('select 0', 'command', 0); + this.trigger_keydown('k'); + this.validate_notebook_state('k at top of notebook', 'command', 0); + this.trigger_keydown('up'); + this.validate_notebook_state('up at top of notebook', 'command', 0); + this.trigger_keydown('down'); + this.validate_notebook_state('down', 'command', 1); + + // Up and down in edit mode + this.click_cell_editor(3); + this.validate_notebook_state('click cell 3', 'edit', 3); + this.trigger_keydown('down'); + this.validate_notebook_state('down at end of notebook', 'edit', 3); + this.set_cell_editor_cursor(3, 0, 0); + this.trigger_keydown('up'); + this.validate_notebook_state('up', 'edit', 2); + this.click_cell_editor(0); + this.validate_notebook_state('click 0', 'edit', 0); + this.trigger_keydown('up'); + this.validate_notebook_state('up at top of notebook', 'edit', 0); + this.set_cell_editor_cursor(0, 0, 10); + this.trigger_keydown('down'); + this.validate_notebook_state('down', 'edit', 1); + }); +}); diff --git a/IPython/html/tests/notebook/dualmode_cellinsert.js b/IPython/html/tests/notebook/dualmode_cellinsert.js new file mode 100644 index 000000000..59b89a329 --- /dev/null +++ b/IPython/html/tests/notebook/dualmode_cellinsert.js @@ -0,0 +1,27 @@ + +// Test +casper.notebook_test(function () { + var a = 'print("a")'; + var index = this.append_cell(a); + this.execute_cell_then(index); + + var b = 'print("b")'; + index = this.append_cell(b); + this.execute_cell_then(index); + + var c = 'print("c")'; + index = this.append_cell(c); + this.execute_cell_then(index); + + this.then(function () { + // Cell insertion + this.select_cell(2); + this.trigger_keydown('a'); // Creates one cell + this.test.assertEquals(this.get_cell_text(2), '', 'a; New cell 2 text is empty'); + this.validate_notebook_state('a', 'command', 2); + this.trigger_keydown('b'); // Creates one cell + this.test.assertEquals(this.get_cell_text(2), '', 'b; Cell 2 text is still empty'); + this.test.assertEquals(this.get_cell_text(3), '', 'b; New cell 3 text is empty'); + this.validate_notebook_state('b', 'command', 3); + }); +}); \ No newline at end of file diff --git a/IPython/html/tests/notebook/dualmode_cellmode.js b/IPython/html/tests/notebook/dualmode_cellmode.js new file mode 100644 index 000000000..d4bf5f018 --- /dev/null +++ b/IPython/html/tests/notebook/dualmode_cellmode.js @@ -0,0 +1,28 @@ +// Test keyboard shortcuts that change the cell's mode. + +// Test +casper.notebook_test(function () { + this.then(function () { + // Cell mode change + this.select_cell(0); + this.trigger_keydown('esc','r'); + this.test.assertEquals(this.get_cell(0).cell_type, 'raw', 'r; cell is raw'); + this.trigger_keydown('1'); + this.test.assertEquals(this.get_cell(0).cell_type, 'heading', '1; cell is heading'); + this.test.assertEquals(this.get_cell(0).level, 1, '1; cell is level 1 heading'); + this.trigger_keydown('2'); + this.test.assertEquals(this.get_cell(0).level, 2, '2; cell is level 2 heading'); + this.trigger_keydown('3'); + this.test.assertEquals(this.get_cell(0).level, 3, '3; cell is level 3 heading'); + this.trigger_keydown('4'); + this.test.assertEquals(this.get_cell(0).level, 4, '4; cell is level 4 heading'); + this.trigger_keydown('5'); + this.test.assertEquals(this.get_cell(0).level, 5, '5; cell is level 5 heading'); + this.trigger_keydown('6'); + this.test.assertEquals(this.get_cell(0).level, 6, '6; cell is level 6 heading'); + this.trigger_keydown('m'); + this.test.assertEquals(this.get_cell(0).cell_type, 'markdown', 'm; cell is markdown'); + this.trigger_keydown('y'); + this.test.assertEquals(this.get_cell(0).cell_type, 'code', 'y; cell is code'); + }); +}); \ No newline at end of file diff --git a/IPython/html/tests/notebook/dualmode_clipboard.js b/IPython/html/tests/notebook/dualmode_clipboard.js new file mode 100644 index 000000000..5068c49c6 --- /dev/null +++ b/IPython/html/tests/notebook/dualmode_clipboard.js @@ -0,0 +1,55 @@ + + +// Test +casper.notebook_test(function () { + var a = 'print("a")'; + var index = this.append_cell(a); + this.execute_cell_then(index); + + var b = 'print("b")'; + index = this.append_cell(b); + this.execute_cell_then(index); + + var c = 'print("c")'; + index = this.append_cell(c); + this.execute_cell_then(index); + + this.then(function () { + // Copy/paste/cut + var num_cells = this.get_cells_length(); + this.test.assertEquals(this.get_cell_text(1), a, 'Verify that cell 1 is a'); + this.select_cell(1); + this.trigger_keydown('x'); // Cut + this.validate_notebook_state('x', 'command', 1); + this.test.assertEquals(this.get_cells_length(), num_cells-1, 'Verify that a cell was removed.'); + this.test.assertEquals(this.get_cell_text(1), b, 'Verify that cell 2 is now where cell 1 was.'); + this.select_cell(2); + this.trigger_keydown('v'); // Paste + this.validate_notebook_state('v', 'command', 3); // Selection should move to pasted cell, below current cell. + this.test.assertEquals(this.get_cell_text(3), a, 'Verify that cell 3 has the cut contents.'); + this.test.assertEquals(this.get_cells_length(), num_cells, 'Verify a the cell was added.'); + this.trigger_keydown('v'); // Paste + this.validate_notebook_state('v', 'command', 4); // Selection should move to pasted cell, below current cell. + this.test.assertEquals(this.get_cell_text(4), a, 'Verify that cell 4 has the cut contents.'); + this.test.assertEquals(this.get_cells_length(), num_cells+1, 'Verify a the cell was added.'); + this.select_cell(1); + this.trigger_keydown('c'); // Copy + this.validate_notebook_state('c', 'command', 1); + this.test.assertEquals(this.get_cell_text(1), b, 'Verify that cell 1 is b'); + this.select_cell(2); + this.trigger_keydown('c'); // Copy + this.validate_notebook_state('c', 'command', 2); + this.test.assertEquals(this.get_cell_text(2), c, 'Verify that cell 2 is c'); + this.select_cell(4); + this.trigger_keydown('v'); // Paste + this.validate_notebook_state('v', 'command', 5); + this.test.assertEquals(this.get_cell_text(2), c, 'Verify that cell 2 still has the copied contents.'); + this.test.assertEquals(this.get_cell_text(5), c, 'Verify that cell 5 has the copied contents.'); + this.test.assertEquals(this.get_cells_length(), num_cells+2, 'Verify a the cell was added.'); + this.select_cell(0); + this.trigger_keydown('shift-v'); // Paste + this.validate_notebook_state('shift-v', 'command', 0); + this.test.assertEquals(this.get_cell_text(0), c, 'Verify that cell 0 has the copied contents.'); + this.test.assertEquals(this.get_cells_length(), num_cells+3, 'Verify a the cell was added.'); + }); +}); \ No newline at end of file diff --git a/IPython/html/tests/notebook/dualmode_execute.js b/IPython/html/tests/notebook/dualmode_execute.js new file mode 100644 index 000000000..f4cd9542f --- /dev/null +++ b/IPython/html/tests/notebook/dualmode_execute.js @@ -0,0 +1,72 @@ +// Test keyboard invoked execution. + +// Test +casper.notebook_test(function () { + var a = 'print("a")'; + var index = this.append_cell(a); + this.execute_cell_then(index); + + var b = 'print("b")'; + index = this.append_cell(b); + this.execute_cell_then(index); + + var c = 'print("c")'; + index = this.append_cell(c); + this.execute_cell_then(index); + + this.then(function () { + + // shift-enter + // last cell in notebook + var base_index = 3; + this.select_cell(base_index); + this.trigger_keydown('shift-enter'); // Creates one cell + this.validate_notebook_state('shift-enter (no cell below)', 'edit', base_index + 1); + // not last cell in notebook & starts in edit mode + this.click_cell_editor(base_index); + this.validate_notebook_state('click cell ' + base_index, 'edit', base_index); + this.trigger_keydown('shift-enter'); + this.validate_notebook_state('shift-enter (cell exists below)', 'command', base_index + 1); + // starts in command mode + this.trigger_keydown('k'); + this.validate_notebook_state('k in comand mode', 'command', base_index); + this.trigger_keydown('shift-enter'); + this.validate_notebook_state('shift-enter (start in command mode)', 'command', base_index + 1); + + // ctrl-enter + // last cell in notebook + base_index++; + this.trigger_keydown('ctrl-enter'); + this.validate_notebook_state('ctrl-enter (no cell below)', 'command', base_index); + // not last cell in notebook & starts in edit mode + this.click_cell_editor(base_index-1); + this.validate_notebook_state('click cell ' + (base_index-1), 'edit', base_index-1); + this.trigger_keydown('ctrl-enter'); + this.validate_notebook_state('ctrl-enter (cell exists below)', 'command', base_index-1); + // starts in command mode + this.trigger_keydown('j'); + this.validate_notebook_state('j in comand mode', 'command', base_index); + this.trigger_keydown('ctrl-enter'); + this.validate_notebook_state('ctrl-enter (start in command mode)', 'command', base_index); + + // alt-enter + // last cell in notebook + this.trigger_keydown('alt-enter'); // Creates one cell + this.validate_notebook_state('alt-enter (no cell below)', 'edit', base_index + 1); + // not last cell in notebook & starts in edit mode + this.click_cell_editor(base_index); + this.validate_notebook_state('click cell ' + base_index, 'edit', base_index); + this.trigger_keydown('alt-enter'); // Creates one cell + this.validate_notebook_state('alt-enter (cell exists below)', 'edit', base_index + 1); + // starts in command mode + this.trigger_keydown('esc', 'k'); + this.validate_notebook_state('k in comand mode', 'command', base_index); + this.trigger_keydown('alt-enter'); // Creates one cell + this.validate_notebook_state('alt-enter (start in command mode)', 'edit', base_index + 1); + + // Notebook will now have 8 cells, the index of the last cell will be 7. + this.test.assertEquals(this.get_cells_length(), 8, '*-enter commands added cells where needed.'); + this.select_cell(7); + this.validate_notebook_state('click cell ' + 7 + ' and esc', 'command', 7); + }); +}); \ No newline at end of file diff --git a/IPython/html/tests/notebook/dualmode_markdown.js b/IPython/html/tests/notebook/dualmode_markdown.js new file mode 100644 index 000000000..d97405723 --- /dev/null +++ b/IPython/html/tests/notebook/dualmode_markdown.js @@ -0,0 +1,39 @@ + +// Test +casper.notebook_test(function () { + var a = 'print("a")'; + var index = this.append_cell(a); + this.execute_cell_then(index); + + this.then(function () { + // Markdown rendering / unredering + this.select_cell(1); + this.validate_notebook_state('select 1', 'command', 1); + this.trigger_keydown('m'); + this.test.assertEquals(this.get_cell(1).cell_type, 'markdown', 'm; cell is markdown'); + this.test.assertEquals(this.get_cell(1).rendered, false, 'm; cell is rendered'); + this.trigger_keydown('enter'); + this.test.assertEquals(this.get_cell(1).rendered, false, 'enter; cell is unrendered'); + this.validate_notebook_state('enter', 'edit', 1); + this.trigger_keydown('ctrl-enter'); + this.test.assertEquals(this.get_cell(1).rendered, true, 'ctrl-enter; cell is rendered'); + this.validate_notebook_state('enter', 'command', 1); + this.trigger_keydown('enter'); + this.test.assertEquals(this.get_cell(1).rendered, false, 'enter; cell is unrendered'); + this.select_cell(0); + this.test.assertEquals(this.get_cell(1).rendered, false, 'select 0; cell 1 is still unrendered'); + this.validate_notebook_state('select 0', 'command', 0); + this.select_cell(1); + this.validate_notebook_state('select 1', 'command', 1); + this.trigger_keydown('ctrl-enter'); + this.test.assertEquals(this.get_cell(1).rendered, true, 'ctrl-enter; cell is rendered'); + this.select_cell(0); + this.validate_notebook_state('select 0', 'command', 0); + this.trigger_keydown('shift-enter'); + this.validate_notebook_state('shift-enter', 'command', 1); + this.test.assertEquals(this.get_cell(1).rendered, true, 'shift-enter; cell is rendered'); + this.trigger_keydown('shift-enter'); // Creates one cell + this.validate_notebook_state('shift-enter', 'edit', 2); + this.test.assertEquals(this.get_cell(1).rendered, true, 'shift-enter; cell is rendered'); + }); +}); \ No newline at end of file diff --git a/IPython/html/tests/notebook/dualmode_merge.js b/IPython/html/tests/notebook/dualmode_merge.js new file mode 100644 index 000000000..573b4575d --- /dev/null +++ b/IPython/html/tests/notebook/dualmode_merge.js @@ -0,0 +1,21 @@ + +// Test +casper.notebook_test(function () { + this.then(function () { + // Split and merge cells + this.select_cell(0); + this.trigger_keydown('a', 'enter'); // Create cell above and enter edit mode. + this.validate_notebook_state('a, enter', 'edit', 0); + this.set_cell_text(0, 'abcd'); + this.set_cell_editor_cursor(0, 0, 2); + this.test.assertEquals(this.get_cell_text(0), 'abcd', 'Verify that cell 0 has the new contents.'); + this.trigger_keydown('ctrl-shift-subtract'); // Split + this.test.assertEquals(this.get_cell_text(0), 'ab', 'split; Verify that cell 0 has the first half.'); + this.test.assertEquals(this.get_cell_text(1), 'cd', 'split; Verify that cell 1 has the second half.'); + this.validate_notebook_state('split', 'edit', 1); + 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.'); + }); +}); \ No newline at end of file diff --git a/IPython/html/tests/notebook/empty_arrow_keys.js b/IPython/html/tests/notebook/empty_arrow_keys.js index 6abed3a96..a949ce53a 100644 --- a/IPython/html/tests/notebook/empty_arrow_keys.js +++ b/IPython/html/tests/notebook/empty_arrow_keys.js @@ -10,12 +10,12 @@ casper.notebook_test(function () { for (i = 0; i < ncells; i++) { IPython.notebook.delete_cell(); } - - // Simulate the "up arrow" and "down arrow" keys. - // - IPython.keyboard.trigger_keydown('up'); - IPython.keyboard.trigger_keydown('down'); + return true; }); + + // Simulate the "up arrow" and "down arrow" keys. + this.trigger_keydown('up'); + this.trigger_keydown('down'); this.test.assertTrue(result, 'Up/down arrow okay in empty notebook.'); }); diff --git a/IPython/html/tests/notebook/execute_code.js b/IPython/html/tests/notebook/execute_code.js index 1af684f25..076d3b70a 100644 --- a/IPython/html/tests/notebook/execute_code.js +++ b/IPython/html/tests/notebook/execute_code.js @@ -22,7 +22,11 @@ casper.notebook_test(function () { var cell = IPython.notebook.get_cell(0); cell.set_text('a=11; print(a)'); cell.clear_output(); - IPython.keyboard.trigger_keydown('shift-enter'); + }); + + this.then(function(){ + + this.trigger_keydown('shift-enter'); }); this.wait_for_output(0); @@ -41,7 +45,10 @@ casper.notebook_test(function () { var cell = IPython.notebook.get_cell(0); cell.set_text('a=12; print(a)'); cell.clear_output(); - IPython.keyboard.trigger_keydown('ctrl-enter'); + }); + + this.then(function(){ + this.trigger_keydown('ctrl-enter'); }); this.wait_for_output(0); diff --git a/IPython/html/tests/notebook/interrupt.js b/IPython/html/tests/notebook/interrupt.js index 2bb87f8a5..7c2912c4c 100644 --- a/IPython/html/tests/notebook/interrupt.js +++ b/IPython/html/tests/notebook/interrupt.js @@ -31,8 +31,8 @@ casper.notebook_test(function () { }); // interrupt using Ctrl-M I keyboard shortcut - this.thenEvaluate( function() { - IPython.keyboard.trigger_keydown('i'); + this.then(function(){ + this.trigger_keydown('i'); }); this.wait_for_output(0); diff --git a/IPython/html/tests/notebook/merge_cells.js b/IPython/html/tests/notebook/merge_cells.js deleted file mode 100644 index 2d8561433..000000000 --- a/IPython/html/tests/notebook/merge_cells.js +++ /dev/null @@ -1,38 +0,0 @@ -// -// Test merging two notebook cells. -// -casper.notebook_test(function() { - var output = this.evaluate(function () { - // Fill in test data. - IPython.notebook.command_mode(); - var set_cell_text = function () { - var cell_one = IPython.notebook.get_selected_cell(); - cell_one.set_text('a = 5'); - - IPython.keyboard.trigger_keydown('b'); - var cell_two = IPython.notebook.get_selected_cell(); - cell_two.set_text('print(a)'); - }; - - // merge_cell_above() - set_cell_text(); - IPython.notebook.merge_cell_above(); - var merged_above = IPython.notebook.get_selected_cell(); - - // merge_cell_below() - set_cell_text(); - IPython.notebook.select(0); - IPython.notebook.merge_cell_below(); - var merged_below = IPython.notebook.get_selected_cell(); - - return { - above: merged_above.get_text(), - below: merged_below.get_text() - }; - }); - - this.test.assertEquals(output.above, 'a = 5\nprint(a)', - 'Successful merge_cell_above().'); - this.test.assertEquals(output.below, 'a = 5\nprint(a)', - 'Successful merge_cell_below().'); -}); diff --git a/IPython/html/tests/notebook/merge_cells_api.js b/IPython/html/tests/notebook/merge_cells_api.js new file mode 100644 index 000000000..9dd2fbdcb --- /dev/null +++ b/IPython/html/tests/notebook/merge_cells_api.js @@ -0,0 +1,43 @@ +// +// Test merging two notebook cells. +// +casper.notebook_test(function() { + var that = this; + var set_cells_text = function () { + that.evaluate(function() { + var cell_one = IPython.notebook.get_selected_cell(); + cell_one.set_text('a = 5'); + }); + + that.trigger_keydown('b'); + + that.evaluate(function() { + var cell_two = IPython.notebook.get_selected_cell(); + cell_two.set_text('print(a)'); + }); + }; + + this.evaluate(function () { + IPython.notebook.command_mode(); + }); + + // merge_cell_above() + set_cells_text(); + var output_above = this.evaluate(function () { + IPython.notebook.merge_cell_above(); + return IPython.notebook.get_selected_cell().get_text(); + }); + + // merge_cell_below() + set_cells_text(); + var output_below = this.evaluate(function() { + IPython.notebook.select(0); + IPython.notebook.merge_cell_below(); + return IPython.notebook.get_selected_cell().get_text(); + }); + + this.test.assertEquals(output_above, 'a = 5\nprint(a)', + 'Successful merge_cell_above().'); + this.test.assertEquals(output_below, 'a = 5\nprint(a)', + 'Successful merge_cell_below().'); +}); diff --git a/IPython/html/tests/util.js b/IPython/html/tests/util.js index 8c49dd240..a572190bc 100644 --- a/IPython/html/tests/util.js +++ b/IPython/html/tests/util.js @@ -2,15 +2,15 @@ // Utility functions for the HTML notebook's CasperJS tests. // -// Get the URL of a notebook server on which to run tests. casper.get_notebook_server = function () { - port = casper.cli.get("port") + // Get the URL of a notebook server on which to run tests. + port = casper.cli.get("port"); port = (typeof port === 'undefined') ? '8888' : port; - return 'http://127.0.0.1:' + port + return 'http://127.0.0.1:' + port; }; -// Create and open a new notebook. casper.open_new_notebook = function () { + // Create and open a new notebook. var baseUrl = this.get_notebook_server(); this.start(baseUrl); this.thenClick('button#new_notebook'); @@ -34,15 +34,15 @@ casper.open_new_notebook = function () { }); }; -// Return whether or not the kernel is running. casper.kernel_running = function kernel_running() { + // Return whether or not the kernel is running. return this.evaluate(function kernel_running() { return IPython.notebook.kernel.running; }); }; -// Shut down the current notebook's kernel. casper.shutdown_current_kernel = function () { + // Shut down the current notebook's kernel. this.thenEvaluate(function() { IPython.notebook.kernel.kill(); }); @@ -50,8 +50,9 @@ casper.shutdown_current_kernel = function () { this.wait(1000); }; -// Delete created notebook. casper.delete_current_notebook = function () { + // Delete created notebook. + // For some unknown reason, this doesn't work?!? this.thenEvaluate(function() { IPython.notebook.delete(); @@ -59,6 +60,7 @@ casper.delete_current_notebook = function () { }; casper.wait_for_busy = function () { + // Waits for the notebook to enter a busy state. this.waitFor(function () { return this.evaluate(function () { return IPython._status == 'busy'; @@ -67,6 +69,7 @@ casper.wait_for_busy = function () { }; casper.wait_for_idle = function () { + // Waits for the notebook to idle. this.waitFor(function () { return this.evaluate(function () { return IPython._status == 'idle'; @@ -74,8 +77,8 @@ casper.wait_for_idle = function () { }); }; -// wait for the nth output in a given cell casper.wait_for_output = function (cell_num, out_num) { + // wait for the nth output in a given cell this.wait_for_idle(); out_num = out_num || 0; this.then(function() { @@ -94,29 +97,29 @@ casper.wait_for_output = function (cell_num, out_num) { }); }; -// wait for a widget msg que to reach 0 -// -// Parameters -// ---------- -// widget_info : object -// Object which contains info related to the widget. The model_id property -// is used to identify the widget. casper.wait_for_widget = function (widget_info) { + // wait for a widget msg que to reach 0 + // + // Parameters + // ---------- + // widget_info : object + // Object which contains info related to the widget. The model_id property + // is used to identify the widget. this.waitFor(function () { var pending = this.evaluate(function (m) { return IPython.notebook.kernel.widget_manager.get_model(m).pending_msgs; }, {m: widget_info.model_id}); - if (pending == 0) { + if (pending === 0) { return true; } else { return false; } }); -} +}; -// return an output of a given cell casper.get_output_cell = function (cell_num, out_num) { + // return an output of a given cell out_num = out_num || 0; var result = casper.evaluate(function (c, o) { var cell = IPython.notebook.get_cell(c); @@ -137,25 +140,33 @@ casper.get_output_cell = function (cell_num, out_num) { } }; -// return the number of cells in the notebook casper.get_cells_length = function () { + // return the number of cells in the notebook var result = casper.evaluate(function () { return IPython.notebook.get_cells().length; - }) + }); return result; }; -// Set the text content of a cell. casper.set_cell_text = function(index, text){ + // Set the text content of a cell. this.evaluate(function (index, text) { var cell = IPython.notebook.get_cell(index); cell.set_text(text); }, index, text); }; -// Inserts a cell at the bottom of the notebook -// Returns the new cell's index. +casper.get_cell_text = function(index){ + // Get the text content of a cell. + return this.evaluate(function (index) { + var cell = IPython.notebook.get_cell(index); + return cell.get_text(); + }, index); +}; + casper.insert_cell_at_bottom = function(cell_type){ + // Inserts a cell at the bottom of the notebook + // Returns the new cell's index. cell_type = cell_type || 'code'; return this.evaluate(function (cell_type) { @@ -164,9 +175,9 @@ casper.insert_cell_at_bottom = function(cell_type){ }, cell_type); }; -// Insert a cell at the bottom of the notebook and set the cells text. -// Returns the new cell's index. casper.append_cell = function(text, cell_type) { + // Insert a cell at the bottom of the notebook and set the cells text. + // Returns the new cell's index. var index = this.insert_cell_at_bottom(cell_type); if (text !== undefined) { this.set_cell_text(index, text); @@ -174,9 +185,9 @@ casper.append_cell = function(text, cell_type) { return index; }; -// Asynchronously executes a cell by index. -// Returns the cell's index. casper.execute_cell = function(index){ + // Asynchronously executes a cell by index. + // Returns the cell's index. var that = this; this.then(function(){ that.evaluate(function (index) { @@ -187,11 +198,11 @@ casper.execute_cell = function(index){ return index; }; -// Synchronously executes a cell by index. -// Optionally accepts a then_callback parameter. then_callback will get called -// when the cell has finished executing. -// Returns the cell's index. casper.execute_cell_then = function(index, then_callback) { + // Synchronously executes a cell by index. + // Optionally accepts a then_callback parameter. then_callback will get called + // when the cell has finished executing. + // Returns the cell's index. var return_val = this.execute_cell(index); this.wait_for_idle(); @@ -206,18 +217,18 @@ casper.execute_cell_then = function(index, then_callback) { return return_val; }; -// Utility function that allows us to easily check if an element exists -// within a cell. Uses JQuery selector to look for the element. casper.cell_element_exists = function(index, selector){ + // Utility function that allows us to easily check if an element exists + // within a cell. Uses JQuery selector to look for the element. return casper.evaluate(function (index, selector) { var $cell = IPython.notebook.get_cell(index).element; return $cell.find(selector).length > 0; }, index, selector); }; -// Utility function that allows us to execute a jQuery function on an -// element within a cell. casper.cell_element_function = function(index, selector, function_name, function_args){ + // Utility function that allows us to execute a jQuery function on an + // element within a cell. return casper.evaluate(function (index, selector, function_name, function_args) { var $cell = IPython.notebook.get_cell(index).element; var $el = $cell.find(selector); @@ -225,8 +236,183 @@ casper.cell_element_function = function(index, selector, function_name, function }, index, selector, function_name, function_args); }; -// Wrap a notebook test to reduce boilerplate. +casper.validate_notebook_state = function(message, mode, cell_index) { + // Validate the entire dual mode state of the notebook. Make sure no more than + // one cell is selected, focused, in edit mode, etc... + + // General tests. + this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(), + message + '; keyboard and notebook modes match'); + // Is the selected cell the only cell that is selected? + if (cell_index!==undefined) { + this.test.assert(this.is_only_cell_selected(cell_index), + message + '; cell ' + cell_index + ' is the only cell selected'); + } + + // Mode specific tests. + if (mode==='command') { + // Are the notebook and keyboard manager in command mode? + this.test.assertEquals(this.get_keyboard_mode(), 'command', + message + '; in command mode'); + // Make sure there isn't a single cell in edit mode. + this.test.assert(this.is_only_cell_edit(null), + message + '; all cells in command mode'); + this.test.assert(this.is_cell_editor_focused(null), + message + '; no cell editors are focused while in command mode'); + + } else if (mode==='edit') { + // Are the notebook and keyboard manager in edit mode? + this.test.assertEquals(this.get_keyboard_mode(), 'edit', + message + '; in edit mode'); + if (cell_index!==undefined) { + // Is the specified cell the only cell in edit mode? + this.test.assert(this.is_only_cell_edit(cell_index), + message + '; cell ' + cell_index + ' is the only cell in edit mode'); + // Is the specified cell the only cell with a focused code mirror? + this.test.assert(this.is_cell_editor_focused(cell_index), + message + '; cell ' + cell_index + '\'s editor is appropriately focused'); + } + + } else { + this.test.assert(false, message + '; ' + mode + ' is an unknown mode'); + } +}; + +casper.select_cell = function(index) { + // Select a cell in the notebook. + this.evaluate(function (i) { + IPython.notebook.select(i); + }, {i: index}); +}; + +casper.click_cell_editor = function(index) { + // Emulate a click on a cell's editor. + + // Code Mirror does not play nicely with emulated brower events. + // Instead of trying to emulate a click, here we run code similar to + // the code used in Code Mirror that handles the mousedown event on a + // region of codemirror that the user can focus. + this.evaluate(function (i) { + var cm = IPython.notebook.get_cell(i).code_mirror; + if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input)) + cm.display.input.focus(); + }, {i: index}); +}; + +casper.set_cell_editor_cursor = function(index, line_index, char_index) { + // Set the Code Mirror instance cursor's location. + this.evaluate(function (i, l, c) { + IPython.notebook.get_cell(i).code_mirror.setCursor(l, c); + }, {i: index, l: line_index, c: char_index}); +}; + +casper.focus_notebook = function() { + // Focus the notebook div. + this.evaluate(function (){ + $('#notebook').focus(); + }, {}); +}; + +casper.trigger_keydown = function() { + // Emulate a keydown in the notebook. + for (var i = 0; i < arguments.length; i++) { + this.evaluate(function (k) { + var element = $(document); + var event = IPython.keyboard.shortcut_to_event(k, 'keydown'); + element.trigger(event); + }, {k: arguments[i]}); + } +}; + +casper.get_keyboard_mode = function() { + // Get the mode of the keyboard manager. + return this.evaluate(function() { + return IPython.keyboard_manager.mode; + }, {}); +}; + +casper.get_notebook_mode = function() { + // Get the mode of the notebook. + return this.evaluate(function() { + return IPython.notebook.mode; + }, {}); +}; + +casper.get_cell = function(index) { + // Get a single cell. + // + // Note: Handles to DOM elements stored in the cell will be useless once in + // CasperJS context. + return this.evaluate(function(i) { + var cell = IPython.notebook.get_cell(i); + if (cell) { + return cell; + } + return null; + }, {i : index}); +}; + +casper.is_cell_editor_focused = function(index) { + // Make sure a cell's editor is the only editor focused on the page. + return this.evaluate(function(i) { + var focused_textarea = $('#notebook .CodeMirror-focused textarea'); + if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; } + if (i === null) { + return focused_textarea.length === 0; + } else { + var cell = IPython.notebook.get_cell(i); + if (cell) { + return cell.code_mirror.getInputField() == focused_textarea[0]; + } + } + return false; + }, {i : index}); +}; + +casper.is_only_cell_selected = function(index) { + // Check if a cell is the only cell selected. + // Pass null as the index to check if no cells are selected. + return this.is_only_cell_on(index, 'selected', 'unselected'); +}; + +casper.is_only_cell_edit = function(index) { + // Check if a cell is the only cell in edit mode. + // Pass null as the index to check if all of the cells are in command mode. + return this.is_only_cell_on(index, 'edit_mode', 'command_mode'); +}; + +casper.is_only_cell_on = function(i, on_class, off_class) { + // Check if a cell is the only cell with the `on_class` DOM class applied to it. + // All of the other cells are checked for the `off_class` DOM class. + // Pass null as the index to check if all of the cells have the `off_class`. + var cells_length = this.get_cells_length(); + for (var j = 0; j < cells_length; j++) { + if (j === i) { + if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) { + return false; + } + } else { + if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) { + return false; + } + } + } + return true; +}; + +casper.cell_has_class = function(index, classes) { + // Check if a cell has a class. + return this.evaluate(function(i, c) { + var cell = IPython.notebook.get_cell(i); + if (cell) { + return cell.element.hasClass(c); + } + return false; + }, {i : index, c: classes}); +}; + casper.notebook_test = function(test) { + // Wrap a notebook test to reduce boilerplate. this.open_new_notebook(); this.then(test); @@ -253,14 +439,14 @@ casper.notebook_test = function(test) { casper.wait_for_dashboard = function () { // Wait for the dashboard list to load. casper.waitForSelector('.list_item'); -} +}; casper.open_dashboard = function () { // Start casper by opening the dashboard page. var baseUrl = this.get_notebook_server(); this.start(baseUrl); this.wait_for_dashboard(); -} +}; casper.dashboard_test = function (test) { // Open the dashboard page and run a test. @@ -276,16 +462,16 @@ casper.dashboard_test = function (test) { this.run(function() { this.test.done(); }); -} +}; -casper.options.waitTimeout=10000 +casper.options.waitTimeout=10000; casper.on('waitFor.timeout', function onWaitForTimeout(timeout) { this.echo("Timeout for " + casper.get_notebook_server()); this.echo("Is the notebook server running?"); }); -// Pass `console.log` calls from page JS to casper. -casper.printLog = function () { +casper.print_log = function () { + // Pass `console.log` calls from page JS to casper. this.on('remote.message', function(msg) { this.echo('Remote message caught: ' + msg); }); diff --git a/docs/source/notebook/index.rst b/docs/source/notebook/index.rst new file mode 100644 index 000000000..2191fda40 --- /dev/null +++ b/docs/source/notebook/index.rst @@ -0,0 +1,13 @@ +==================== +The IPython notebook +==================== + +.. toctree:: + :maxdepth: 2 + + notebook + cm_keyboard + nbconvert + public_server + security + diff --git a/docs/source/notebook/ipython_security.asc b/docs/source/notebook/ipython_security.asc new file mode 100644 index 000000000..95436812a --- /dev/null +++ b/docs/source/notebook/ipython_security.asc @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2.0.22 (GNU/Linux) + +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 +5vmLc+Ibz+eT+rM= +=XVDe +-----END PGP PUBLIC KEY BLOCK----- diff --git a/docs/source/interactive/nbconvert.rst b/docs/source/notebook/nbconvert.rst similarity index 100% rename from docs/source/interactive/nbconvert.rst rename to docs/source/notebook/nbconvert.rst diff --git a/docs/source/interactive/notebook.rst b/docs/source/notebook/notebook.rst similarity index 100% rename from docs/source/interactive/notebook.rst rename to docs/source/notebook/notebook.rst diff --git a/docs/source/interactive/public_server.rst b/docs/source/notebook/public_server.rst similarity index 99% rename from docs/source/interactive/public_server.rst rename to docs/source/notebook/public_server.rst index aae62c8e7..9454cd05c 100644 --- a/docs/source/interactive/public_server.rst +++ b/docs/source/notebook/public_server.rst @@ -19,8 +19,8 @@ a public interface `. .. _notebook_security: -Notebook security ------------------ +Securing a notebook server +-------------------------- You can protect your notebook server with a simple single password by setting the :attr:`NotebookApp.password` configurable. You can prepare a diff --git a/docs/source/notebook/security.rst b/docs/source/notebook/security.rst new file mode 100644 index 000000000..fa68579bc --- /dev/null +++ b/docs/source/notebook/security.rst @@ -0,0 +1,146 @@ +Security in IPython notebooks +============================= + +As IPython notebooks become more popular for sharing and collaboration, +the potential for malicious people to attempt to exploit the notebook +for their nefarious purposes increases. IPython 2.0 introduces a +security model to prevent execution of untrusted code without explicit +user input. + +The problem +----------- + +The whole point of IPython is arbitrary code execution. We have no +desire to limit what can be done with a notebook, which would negatively +impact its utility. + +Unlike other programs, an IPython notebook document includes output. +Unlike other documents, that output exists in a context that can execute +code (via Javascript). + +The security problem we need to solve is that no code should execute +just because a user has **opened** a notebook that **they did not +write**. Like any other program, once a user decides to execute code in +a notebook, it is considered trusted, and should be allowed to do +anything. + +Our security model +------------------ + +- Untrusted HTML is always sanitized +- Untrusted Javascript is never executed +- HTML and Javascript in Markdown cells are never trusted +- **Outputs** generated by the user are trusted +- Any other HTML or Javascript (in Markdown cells, output generated by + others) is never trusted +- The central question of trust is "Did the current user do this?" + +The details of trust +-------------------- + +IPython notebooks store a signature in metadata, which is used to answer +the question "Did the current user do this?" + +This signature is a digest of the notebooks contents plus a secret key, +known only to the user. The secret key is a user-only readable file in +the IPython profile's security directory. By default, this is:: + + ~/.ipython/profile_default/security/notebook_secret + +When a notebook is opened by a user, the server computes a signature +with the user's key, and compares it with the signature stored in the +notebook's metadata. If the signature matches, HTML and Javascript +output in the notebook will be trusted at load, otherwise it will be +untrusted. + +Any output generated during an interactive session is trusted. + +Updating trust +************** + +A notebook's trust is updated when the notebook is saved. If there are +any untrusted outputs still in the notebook, the notebook will not be +trusted, and no signature will be stored. If all untrusted outputs have +been removed (either via ``Clear Output`` or re-execution), then the +notebook will become trusted. + +While trust is updated per output, this is only for the duration of a +single session. A notebook file on disk is either trusted or not in its +entirety. + +Explicit trust +************** + +Sometimes re-executing a notebook to generate trusted output is not an +option, either because dependencies are unavailable, or it would take a +long time. Users can explicitly trust a notebook in two ways: + +- At the command-line, with:: + + ipython trust /path/to/notebook.ipynb + +- After loading the untrusted notebook, with ``File / Trust Notebook`` + +These two methods simply load the notebook, compute a new signature with +the user's key, and then store the newly signed notebook. + +Reporting security issues +------------------------- + +If you find a security vulnerability in IPython, either a failure of the +code to properly implement the model described here, or a failure of the +model itself, please report it to security@ipython.org. + +If you prefer to encrypt your security reports, +you can use :download:`this PGP public key `. + +Affected use cases +------------------ + +Some use cases that work in IPython 1.0 will become less convenient in +2.0 as a result of the security changes. We do our best to minimize +these annoyance, but security is always at odds with convenience. + +Javascript and CSS in Markdown cells +************************************ + +While never officially supported, it had become common practice to put +hidden Javascript or CSS styling in Markdown cells, so that they would +not be visible on the page. Since Markdown cells are now sanitized (by +`Google Caja `__), all Javascript +(including click event handlers, etc.) and CSS will be stripped. + +We plan to provide a mechanism for notebook themes, but in the meantime +styling the notebook can only be done via either ``custom.css`` or CSS +in HTML output. The latter only have an effect if the notebook is +trusted, because otherwise the output will be sanitized just like +Markdown. + +Collaboration +************* + +When collaborating on a notebook, people probably want to see the +outputs produced by their colleagues' most recent executions. Since each +collaborator's key will differ, this will result in each share starting +in an untrusted state. There are three basic approaches to this: + +- re-run notebooks when you get them (not always viable) +- explicitly trust notebooks via ``ipython trust`` or the notebook menu + (annoying, but easy) +- share a notebook secret, and use an IPython profile dedicated to the + collaboration while working on the project. + +Multiple profiles or machines +***************************** + +Since the notebook secret is stored in a profile directory by default, +opening a notebook with a different profile or on a different machine +will result in a different key, and thus be untrusted. The only current +way to address this is by sharing the notebook secret. This can be +facilitated by setting the configurable: + +.. sourcecode:: python + + c.NotebookApp.secret_file = "/path/to/notebook_secret" + +in each profile, and only sharing the secret once per machine. diff --git a/setup.py b/setup.py index 07ea5a5d4..7ef9289ff 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ requires utilities which are not available under Windows.""" # # Distributed under the terms of the Modified BSD License. # -# The full license is in the file COPYING.txt, distributed with this software. +# The full license is in the file COPYING.rst, distributed with this software. #----------------------------------------------------------------------------- #----------------------------------------------------------------------------- diff --git a/setupbase.py b/setupbase.py index 1d474e46e..21a285781 100644 --- a/setupbase.py +++ b/setupbase.py @@ -161,6 +161,7 @@ def find_package_data(): pjoin(components, "jquery", "jquery.min.js"), pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"), pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"), + pjoin(components, "jquery-ui", "themes", "smoothness", "images", "*"), pjoin(components, "marked", "lib", "marked.js"), pjoin(components, "requirejs", "require.js"), pjoin(components, "underscore", "underscore-min.js"),