From a9b4bdc03f8c7882eb79aeaf5bb98cd946e64289 Mon Sep 17 00:00:00 2001 From: Matthias BUSSONNIER Date: Fri, 5 Sep 2014 11:01:10 -0700 Subject: [PATCH 001/588] Move md-cell display logic to css --- IPython/html/static/notebook/js/textcell.js | 7 ------- IPython/html/static/notebook/less/textcell.less | 9 +++++++++ IPython/html/static/style/ipython.min.css | 6 ++++++ IPython/html/static/style/style.min.css | 6 ++++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/IPython/html/static/notebook/js/textcell.js b/IPython/html/static/notebook/js/textcell.js index e599a9958..628a2f279 100644 --- a/IPython/html/static/notebook/js/textcell.js +++ b/IPython/html/static/notebook/js/textcell.js @@ -130,8 +130,6 @@ define([ if (cont) { var text_cell = this.element; var output = text_cell.find("div.text_cell_render"); - output.hide(); - text_cell.find('div.input_area').show(); if (this.get_text() === this.placeholder) { this.set_text(''); } @@ -260,8 +258,6 @@ define([ // links in markdown cells should open in new tabs html.find("a[href]").not('[href^="#"]').attr("target", "_blank"); this.set_rendered(html); - this.element.find('div.input_area').hide(); - this.element.find("div.text_cell_render").show(); this.typeset(); } return cont; @@ -283,7 +279,6 @@ define([ TextCell.apply(this, [$.extend({}, options, {config: config})]); // RawCell should always hide its rendered div - this.element.find('div.text_cell_render').hide(); this.cell_type = 'raw'; }; @@ -426,8 +421,6 @@ define([ .text('ΒΆ') ); this.set_rendered(h); - this.element.find('div.input_area').hide(); - this.element.find("div.text_cell_render").show(); this.typeset(); } return cont; diff --git a/IPython/html/static/notebook/less/textcell.less b/IPython/html/static/notebook/less/textcell.less index 67e14ad19..bacb486df 100644 --- a/IPython/html/static/notebook/less/textcell.less +++ b/IPython/html/static/notebook/less/textcell.less @@ -36,6 +36,15 @@ div.cell.text_cell.rendered { padding: 0px; } + +.text_cell.rendered .input_area { + display: none; +} + +.text_cell.unrendered .text_cell_render { + display:none; +} + .cm-s-heading-1, .cm-s-heading-2, .cm-s-heading-3, diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index aa541e87d..e4e0994cf 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -1170,6 +1170,12 @@ h6:hover .anchor-link { div.cell.text_cell.rendered { padding: 0px; } +.text_cell.rendered .input_area { + display: none; +} +.text_cell.unrendered .text_cell_render { + display: none; +} .cm-s-heading-1, .cm-s-heading-2, .cm-s-heading-3, diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index 41b21ceba..ce6f57735 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -8942,6 +8942,12 @@ h6:hover .anchor-link { div.cell.text_cell.rendered { padding: 0px; } +.text_cell.rendered .input_area { + display: none; +} +.text_cell.unrendered .text_cell_render { + display: none; +} .cm-s-heading-1, .cm-s-heading-2, .cm-s-heading-3, From 186c8ae41d90a2ee11546c6fa2650e19a1fd5ee2 Mon Sep 17 00:00:00 2001 From: Matthias BUSSONNIER Date: Fri, 5 Sep 2014 11:14:37 -0700 Subject: [PATCH 002/588] remove useless comment --- IPython/html/static/notebook/js/textcell.js | 1 - 1 file changed, 1 deletion(-) diff --git a/IPython/html/static/notebook/js/textcell.js b/IPython/html/static/notebook/js/textcell.js index 628a2f279..c3af16376 100644 --- a/IPython/html/static/notebook/js/textcell.js +++ b/IPython/html/static/notebook/js/textcell.js @@ -278,7 +278,6 @@ define([ var config = this.mergeopt(RawCell, options.config); TextCell.apply(this, [$.extend({}, options, {config: config})]); - // RawCell should always hide its rendered div this.cell_type = 'raw'; }; From 9c1c4f9f0a10646ce4a87cc30f34101cd3deac42 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Mon, 15 Sep 2014 22:24:49 -0700 Subject: [PATCH 003/588] Fix bug in bounded int/float logic. --- IPython/html/widgets/widget_int.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/IPython/html/widgets/widget_int.py b/IPython/html/widgets/widget_int.py index 577728791..fa96676da 100644 --- a/IPython/html/widgets/widget_int.py +++ b/IPython/html/widgets/widget_int.py @@ -37,13 +37,23 @@ class _BoundedInt(_Int): def __init__(self, *pargs, **kwargs): """Constructor""" DOMWidget.__init__(self, *pargs, **kwargs) - self.on_trait_change(self._validate, ['value', 'min', 'max']) + self.on_trait_change(self._validate_value, ['value']) + self.on_trait_change(self._handle_max_changed, ['max']) + self.on_trait_change(self._handle_min_changed, ['min']) - def _validate(self, name, old, new): - """Validate value, max, min.""" + def _validate_value(self, name, old, new): + """Validate value.""" if self.min > new or new > self.max: self.value = min(max(new, self.min), self.max) + def _handle_max_changed(self, name, old, new): + """Make sure the min is always <= the max.""" + self.min = min(self.min, new) + + def _handle_min_changed(self, name, old, new): + """Make sure the max is always >= the min.""" + self.max = max(self.max, new) + class IntText(_Int): """Textbox widget that represents a int.""" From fe99c27175f607d2c33cabb358f287a9560e6d1f Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 21 Sep 2014 11:16:37 +0200 Subject: [PATCH 004/588] remove cython extension. Now in cython package itself, as stable. --- IPython/testing/iptest.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/IPython/testing/iptest.py b/IPython/testing/iptest.py index b91100987..840d9ebfd 100644 --- a/IPython/testing/iptest.py +++ b/IPython/testing/iptest.py @@ -141,7 +141,6 @@ have['pymongo'] = test_for('pymongo') have['pygments'] = test_for('pygments') have['qt'] = test_for('IPython.external.qt') have['sqlite3'] = test_for('sqlite3') -have['cython'] = test_for('Cython') have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None) have['jinja2'] = test_for('jinja2') have['mistune'] = test_for('mistune') @@ -251,9 +250,6 @@ test_sections['kernel.inprocess'].requires('zmq') # extensions: sec = test_sections['extensions'] -if not have['cython']: - sec.exclude('cythonmagic') - sec.exclude('tests.test_cythonmagic') # This is deprecated in favour of rpy2 sec.exclude('rmagic') # autoreload does some strange stuff, so move it to its own test section From aa04d40a8d895b76ab3b13fff96d2dcbc478516e Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Tue, 23 Sep 2014 08:40:43 -0700 Subject: [PATCH 005/588] Added test --- IPython/html/tests/widgets/widget_int.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/IPython/html/tests/widgets/widget_int.js b/IPython/html/tests/widgets/widget_int.js index a9bc43d90..9f06d6dff 100644 --- a/IPython/html/tests/widgets/widget_int.js +++ b/IPython/html/tests/widgets/widget_int.js @@ -154,4 +154,24 @@ casper.notebook_test(function () { this.test.assertEquals(this.get_output_cell(index).text, '50\n', 'Invalid int textbox characters ignored'); }); + + index = this.append_cell( + 'a = widgets.IntSlider()\n' + + 'display(a)\n' + + 'a.max = -1\n' + + 'print("Success")\n'); + this.execute_cell_then(index, function(index){ + this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', + 'Invalid int range max bound does not cause crash.'); + }); + + index = this.append_cell( + 'a = widgets.IntSlider()\n' + + 'display(a)\n' + + 'a.min = 101\n' + + 'print("Success")\n'); + this.execute_cell_then(index, function(index){ + this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', + 'Invalid int range min bound does not cause crash.'); + }); }); \ No newline at end of file From 6a976cf2b6824e5fceae5bb4e56eb6b4200e4d3d Mon Sep 17 00:00:00 2001 From: Gordon Ball Date: Wed, 9 Jul 2014 16:13:38 +0200 Subject: [PATCH 006/588] Use contentEditable to allow modification via the the slider readout --- IPython/html/static/widgets/js/widget_int.js | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index 48566c535..2369ce29d 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -29,6 +29,7 @@ define([ this.$readout = $('
') .appendTo(this.$el) .addClass('widget-readout') + .attr('contentEditable', true) .hide(); this.model.on('change:slider_color', function(sender, value) { @@ -156,9 +157,40 @@ define([ events: { // Dictionary of events and their handlers. - "slide" : "handleSliderChange" + "slide" : "handleSliderChange", + "blur [contentEditable=true]": "handleTextChange" }, + handleTextChange: function(e) { + var text = $(e.target).text().trim(); + var value = this._validate_text_input(text); + if (isNaN(value)) { + this.$readout.text(this.model.get('value')); + } else { + //check for outside range + if (value > this.model.get('max')) value = this.model.get('max'); + if (value < this.model.get('min')) value = this.model.get('min'); + + //update the readout unconditionally + //this covers eg, entering a float value which rounds to the + //existing int value, which will not trigger an update since the model + //doesn't change, but we should update the text to reflect that + //a float value isn't being used + this.$readout.text(value); + + //note that the step size currently isn't enforced, so if an + //off-step value is input it will be retained + + //update the model + this.model.set('value', value, {updated_view: this}); + this.touch(); + } + }, + + _validate_text_input: function(x) { + return parseInt(x); + }, + handleSliderChange: function(e, ui) { // Called when the slider value is changed. From 0b45bacd0dca9d1aa168c43c5329737cc7f7a9e7 Mon Sep 17 00:00:00 2001 From: Gordon Ball Date: Wed, 9 Jul 2014 16:14:06 +0200 Subject: [PATCH 007/588] Add support to the float slider --- IPython/html/static/widgets/js/widget_float.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/IPython/html/static/widgets/js/widget_float.js b/IPython/html/static/widgets/js/widget_float.js index b5d808f91..128c135dc 100644 --- a/IPython/html/static/widgets/js/widget_float.js +++ b/IPython/html/static/widgets/js/widget_float.js @@ -9,6 +9,10 @@ define([ var IntTextView = int_widgets.IntTextView; var FloatSliderView = IntSliderView.extend({ + _validate_text_input: function(x) { + return parseFloat(x); + }, + _validate_slide_value: function(x) { // Validate the value of the slider before sending it to the back-end // and applying it to the other views on the page. From 0f7fbc07813dbc73d71031ff12f14ae30dc3526b Mon Sep 17 00:00:00 2001 From: Gordon Ball Date: Wed, 9 Jul 2014 23:26:25 +0200 Subject: [PATCH 008/588] Add keydown listener to commit changes on --- IPython/html/static/widgets/js/widget_int.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index 2369ce29d..48ec6524e 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -158,9 +158,17 @@ define([ events: { // Dictionary of events and their handlers. "slide" : "handleSliderChange", - "blur [contentEditable=true]": "handleTextChange" + "blur [contentEditable=true]": "handleTextChange", + "keydown [contentEditable=true]": "handleKeyDown" }, + handleKeyDown: function(e) { + if (e.keyCode == 13) { + e.preventDefault(); + this.handleTextChange(e); + } + }, + handleTextChange: function(e) { var text = $(e.target).text().trim(); var value = this._validate_text_input(text); From 67630b0886b38433ff33f09578bb01962ce38d05 Mon Sep 17 00:00:00 2001 From: Gordon Ball Date: Wed, 9 Jul 2014 23:45:43 +0200 Subject: [PATCH 009/588] Ignore the event object for handleTextChange --- IPython/html/static/widgets/js/widget_int.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index 48ec6524e..73abd3d53 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -165,12 +165,12 @@ define([ handleKeyDown: function(e) { if (e.keyCode == 13) { e.preventDefault(); - this.handleTextChange(e); + this.handleTextChange(); } }, - handleTextChange: function(e) { - var text = $(e.target).text().trim(); + handleTextChange: function() { + var text = this.$readout.text(); var value = this._validate_text_input(text); if (isNaN(value)) { this.$readout.text(this.model.get('value')); From 3011820cd3dfedf85967a1270ee876a6c927d5d3 Mon Sep 17 00:00:00 2001 From: Gordon Ball Date: Tue, 26 Aug 2014 10:08:50 +0200 Subject: [PATCH 010/588] Add support for parsing pairs of numbers for range sliders --- .../html/static/widgets/js/widget_float.js | 6 +- IPython/html/static/widgets/js/widget_int.js | 82 +++++++++++++------ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/IPython/html/static/widgets/js/widget_float.js b/IPython/html/static/widgets/js/widget_float.js index 128c135dc..dec3afd2c 100644 --- a/IPython/html/static/widgets/js/widget_float.js +++ b/IPython/html/static/widgets/js/widget_float.js @@ -9,9 +9,9 @@ define([ var IntTextView = int_widgets.IntTextView; var FloatSliderView = IntSliderView.extend({ - _validate_text_input: function(x) { - return parseFloat(x); - }, + _parse_text_input: parseFloat, + + _range_regex: /^\s*([+-]?\d*\.?\d+)\s*[-:]\s*([+-]?\d*\.?\d+)/, _validate_slide_value: function(x) { // Validate the value of the slider before sending it to the back-end diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index 73abd3d53..e8f759c81 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -170,34 +170,70 @@ define([ }, handleTextChange: function() { + // this handles the entry of text into the contentEditable label + // first, the value is checked if it contains a parseable number + // (or pair of numbers, for the _range case) + // then it is clamped within the min-max range of the slider + // finally, the model is updated if the value is to be changed + // + // if any of these conditions are not met, the text is reset + // + // the step size is not enforced + var text = this.$readout.text(); - var value = this._validate_text_input(text); - if (isNaN(value)) { - this.$readout.text(this.model.get('value')); + var vmin = this.model.get('min'); + var vmax = this.model.get('max'); + if (this.model.get("_range")) { + // range case + // ranges can be expressed either "val-val" or "val:val" (+spaces) + var match = this._range_regex.exec(text); + if (match) { + var values = [this._parse_text_input(match[1]), + this._parse_text_input(match[2])]; + // reject input where NaN or lower > upper + if (isNaN(values[0]) || + isNaN(values[1]) || + (values[0] > values[1])) { + this.$readout.text(this.model.get('value').join('-')); + } else { + // clamp to range + values = [Math.max(Math.min(values[0], vmax), vmin), + Math.max(Math.min(values[1], vmax), vmin)]; + + if ((values[0] != this.model.get('value')[0]) || + (values[1] != this.model.get('value')[1])) { + this.$readout.text(values.join('-')); + this.model.set('value', values, {updated_view: this}); + this.touch(); + } else { + this.$readout.text(this.model.get('value').join('-')); + } + } + } else { + this.$readout.text(this.model.get('value').join('-')); + } } else { - //check for outside range - if (value > this.model.get('max')) value = this.model.get('max'); - if (value < this.model.get('min')) value = this.model.get('min'); - - //update the readout unconditionally - //this covers eg, entering a float value which rounds to the - //existing int value, which will not trigger an update since the model - //doesn't change, but we should update the text to reflect that - //a float value isn't being used - this.$readout.text(value); - - //note that the step size currently isn't enforced, so if an - //off-step value is input it will be retained - - //update the model - this.model.set('value', value, {updated_view: this}); - this.touch(); + // single value case + var value = this._parse_text_input(text); + if (isNaN(value)) { + this.$readout.text(this.model.get('value')); + } else { + value = Math.max(Math.min(value, vmax), vmin); + + if (value != this.model.get('value')) { + this.$readout.text(value); + this.model.set('value', value, {updated_view: this}); + this.touch(); + } else { + this.$readout.text(this.model.get('value')); + } + } } }, - _validate_text_input: function(x) { - return parseInt(x); - }, + _parse_text_input: parseInt, + + _range_regex: /^\s*([+-]?\d+)\s*[-:]\s*([+-]?\d+)/, handleSliderChange: function(e, ui) { // Called when the slider value is changed. From 3ec873b084ca217608187824c57553b0bfb5b14d Mon Sep 17 00:00:00 2001 From: Gordon Ball Date: Tue, 16 Sep 2014 11:28:10 +0200 Subject: [PATCH 011/588] Change _parse_text_input to _parse_value and update float range regex --- IPython/html/static/widgets/js/widget_float.js | 10 ++++------ IPython/html/static/widgets/js/widget_int.js | 13 +++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/IPython/html/static/widgets/js/widget_float.js b/IPython/html/static/widgets/js/widget_float.js index dec3afd2c..97d8f0fdd 100644 --- a/IPython/html/static/widgets/js/widget_float.js +++ b/IPython/html/static/widgets/js/widget_float.js @@ -9,9 +9,10 @@ define([ var IntTextView = int_widgets.IntTextView; var FloatSliderView = IntSliderView.extend({ - _parse_text_input: parseFloat, + _parse_value: parseFloat, - _range_regex: /^\s*([+-]?\d*\.?\d+)\s*[-:]\s*([+-]?\d*\.?\d+)/, + // matches: whitespace?, float, whitespace?, [-:], whitespace?, float + _range_regex: /^\s*([+-]?(?:\d*\.?\d+|\d+\.)(?:[eE][+-]?\d+)?)\s*[-:]\s*([+-]?(?:\d*\.?\d+|\d+\.)(?:[eE][+-]?\d+)?)/, _validate_slide_value: function(x) { // Validate the value of the slider before sending it to the back-end @@ -21,10 +22,7 @@ define([ }); var FloatTextView = IntTextView.extend({ - _parse_value: function(value) { - // Parse the value stored in a string. - return parseFloat(value); - }, + _parse_value: parseFloat }); return { diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index e8f759c81..148b8b75e 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -188,8 +188,8 @@ define([ // ranges can be expressed either "val-val" or "val:val" (+spaces) var match = this._range_regex.exec(text); if (match) { - var values = [this._parse_text_input(match[1]), - this._parse_text_input(match[2])]; + var values = [this._parse_value(match[1]), + this._parse_value(match[2])]; // reject input where NaN or lower > upper if (isNaN(values[0]) || isNaN(values[1]) || @@ -214,7 +214,7 @@ define([ } } else { // single value case - var value = this._parse_text_input(text); + var value = this._parse_value(text); if (isNaN(value)) { this.$readout.text(this.model.get('value')); } else { @@ -231,7 +231,7 @@ define([ } }, - _parse_text_input: parseInt, + _parse_value: parseInt, _range_regex: /^\s*([+-]?\d+)\s*[-:]\s*([+-]?\d+)/, @@ -362,10 +362,7 @@ define([ } }, - _parse_value: function(value) { - // Parse the value stored in a string. - return parseInt(value); - }, + _parse_value: parseInt }); From aec576a5f2f5641ad57038a61de8a05c2a61c2b5 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Tue, 23 Sep 2014 12:25:02 -0700 Subject: [PATCH 012/588] Fix infinite loop typo --- IPython/html/static/widgets/js/widget_int.js | 10 +++++++++- IPython/html/tests/widgets/widget_int.js | 6 ++---- IPython/html/widgets/widget.py | 21 +++++++++++++------- IPython/html/widgets/widget_int.py | 9 ++++----- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index c8a736b86..24103cc85 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -43,7 +43,7 @@ define([ if (options === undefined || options.updated_view != this) { // JQuery slider option keys. These keys happen to have a // one-to-one mapping with the corrosponding keys of the model. - var jquery_slider_keys = ['step', 'max', 'min', 'disabled']; + var jquery_slider_keys = ['step', 'disabled']; var that = this; that.$slider.slider({}); _.each(jquery_slider_keys, function(key, i) { @@ -52,6 +52,14 @@ define([ that.$slider.slider("option", key, model_value); } }); + + var max = this.model.get('max'); + var min = this.model.get('min'); + if (min <= max) { + if (max !== undefined) this.$slider.slider('option', 'max', max); + if (min !== undefined) this.$slider.slider('option', 'min', min); + } + var range_value = this.model.get("_range"); if (range_value !== undefined) { this.$slider.slider("option", "range", range_value); diff --git a/IPython/html/tests/widgets/widget_int.js b/IPython/html/tests/widgets/widget_int.js index 9f06d6dff..b5170bab1 100644 --- a/IPython/html/tests/widgets/widget_int.js +++ b/IPython/html/tests/widgets/widget_int.js @@ -161,8 +161,7 @@ casper.notebook_test(function () { 'a.max = -1\n' + 'print("Success")\n'); this.execute_cell_then(index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', - 'Invalid int range max bound does not cause crash.'); + this.test.assertEquals(0, 0, 'Invalid int range max bound does not cause crash.'); }); index = this.append_cell( @@ -171,7 +170,6 @@ casper.notebook_test(function () { 'a.min = 101\n' + 'print("Success")\n'); this.execute_cell_then(index, function(index){ - this.test.assertEquals(this.get_output_cell(index).text, 'Success\n', - 'Invalid int range min bound does not cause crash.'); + this.test.assertEquals(0, 0, 'Invalid int range min bound does not cause crash.'); }); }); \ No newline at end of file diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index 70a3e4f90..1739bd04a 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -124,7 +124,6 @@ class Widget(LoggingConfigurable): self._model_id = kwargs.pop('model_id', None) super(Widget, self).__init__(**kwargs) - self.on_trait_change(self._handle_property_changed, self.keys) Widget._call_widget_constructed(self) self.open() @@ -322,13 +321,21 @@ class Widget(LoggingConfigurable): def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) - - def _handle_property_changed(self, name, old, new): + + def _notify_trait(self, name, old_value, new_value): """Called when a property has been changed.""" - # Make sure this isn't information that the front-end just sent us. - if self._should_send_property(name, new): - # Send new state to front-end - self.send_state(key=name) + # Trigger default traitlet callback machinery. This allows any user + # registered validation to be processed prior to allowing the widget + # machinery to handle the state. + super(Widget, self)._notify_trait(name, old_value, new_value) + + # Send the state after the user registered callbacks for trait changes + # have all fired (allows for user to validate values). + if name in self.keys: + # Make sure this isn't information that the front-end just sent us. + if self._should_send_property(name, new_value): + # Send new state to front-end + self.send_state(key=name) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" diff --git a/IPython/html/widgets/widget_int.py b/IPython/html/widgets/widget_int.py index fa96676da..e371e0869 100644 --- a/IPython/html/widgets/widget_int.py +++ b/IPython/html/widgets/widget_int.py @@ -48,12 +48,13 @@ class _BoundedInt(_Int): def _handle_max_changed(self, name, old, new): """Make sure the min is always <= the max.""" - self.min = min(self.min, new) + if new < self.min: + raise ValueError("setting max < min") def _handle_min_changed(self, name, old, new): """Make sure the max is always >= the min.""" - self.max = max(self.max, new) - + if new > self.max: + raise ValueError("setting min > max") class IntText(_Int): """Textbox widget that represents a int.""" @@ -137,11 +138,9 @@ class _BoundedIntRange(_IntRange): if name == "min": if new > self.max: raise ValueError("setting min > max") - self.min = new elif name == "max": if new < self.min: raise ValueError("setting max < min") - self.max = new low, high = self.value if name == "value": From f420cdf3e7245c7038fd1c109688458df0f86ffd Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 24 Sep 2014 17:38:04 -0700 Subject: [PATCH 013/588] Make comm_manager a property of kernel, not shell --- IPython/html/widgets/tests/test_interaction.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IPython/html/widgets/tests/test_interaction.py b/IPython/html/widgets/tests/test_interaction.py index 6f04beece..e891c96f4 100644 --- a/IPython/html/widgets/tests/test_interaction.py +++ b/IPython/html/widgets/tests/test_interaction.py @@ -22,6 +22,9 @@ from IPython.utils.py3compat import annotate class DummyComm(Comm): comm_id = 'a-b-c-d' + def open(self, *args, **kwargs): + pass + def send(self, *args, **kwargs): pass From 341527779be2b4fd34eb38b3aec705487ee60ce0 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Thu, 25 Sep 2014 14:51:38 -0700 Subject: [PATCH 014/588] Fix notify_trait getting called too early. --- IPython/html/widgets/widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index 1739bd04a..fd0288387 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -327,11 +327,11 @@ class Widget(LoggingConfigurable): # Trigger default traitlet callback machinery. This allows any user # registered validation to be processed prior to allowing the widget # machinery to handle the state. - super(Widget, self)._notify_trait(name, old_value, new_value) + LoggingConfigurable._notify_trait(self, name, old_value, new_value) # Send the state after the user registered callbacks for trait changes # have all fired (allows for user to validate values). - if name in self.keys: + if self.comm is not None and name in self.keys: # Make sure this isn't information that the front-end just sent us. if self._should_send_property(name, new_value): # Send new state to front-end From 990425f034461e0138f8fcf965ce8a07c11874ba Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 16:14:28 -0700 Subject: [PATCH 015/588] Handle 'deletable' cell metadata --- IPython/html/static/notebook/js/cell.js | 21 ++++++++++++++++----- IPython/html/static/notebook/js/notebook.js | 10 +++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/IPython/html/static/notebook/js/cell.js b/IPython/html/static/notebook/js/cell.js index e695b3e54..d764ab786 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,32 @@ 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? (true by default) + * @method is_deletable + **/ + Cell.prototype.is_deletable = function () { + if (this.metadata.deletable === undefined) { + return true; + } + return Boolean(this.metadata.deletable); + }; /** * @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 ac8b220a3..4235774d8 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -765,7 +765,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)) { @@ -1193,6 +1197,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(); }; From a018cb42994b11132ec7a60d8b24923e1be39224 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 16:14:49 -0700 Subject: [PATCH 016/588] Add tests for undeletable cells --- IPython/html/tests/notebook/deletecell.js | 118 ++++++++++++++++++ IPython/html/tests/notebook/dualmode_merge.js | 118 +++++++++++++++++- 2 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 IPython/html/tests/notebook/deletecell.js diff --git a/IPython/html/tests/notebook/deletecell.js b/IPython/html/tests/notebook/deletecell.js new file mode 100644 index 000000000..477a8bddc --- /dev/null +++ b/IPython/html/tests/notebook/deletecell.js @@ -0,0 +1,118 @@ + +// 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; + 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 not 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 fail) + this.then(function () { + this.select_cell(1); + this.trigger_keydown('d', 'd'); + this.test.assertEquals(this.get_cells_length(), 3, 'Delete cell 1: There are still 3 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), b, 'Delete cell 1: Cell 1 was not deleted'); + this.test.assertEquals(this.get_cell_text(2), c, 'Delete cell 1: Cell 2 was not affected'); + this.validate_notebook_state('dd', 'command', 1); + }); + + // Try to delete cell 2 (should succeed) + this.then(function () { + this.select_cell(2); + this.trigger_keydown('d', 'd'); + this.test.assertEquals(this.get_cells_length(), 2, 'Delete cell 2: There are now 2 cells'); + this.test.assertEquals(this.get_cell_text(0), a, 'Delete cell 2: Cell 0 was not affected'); + this.test.assertEquals(this.get_cell_text(1), b, 'Delete cell 2: Cell 1 was not affected'); + this.validate_notebook_state('dd', 'command', 1); + }); + + // Change the deletable status of the last two cells + this.thenEvaluate(function() { + IPython.notebook.get_cell(0).metadata.deletable = true; + IPython.notebook.get_cell(1).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 deletable'); + + // Try to delete cell 1 (should succeed) + 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 1: Cell 0 was not affected'); + this.validate_notebook_state('dd', 'command', 0); + + // 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 573b4575d..8ec32405f 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 +}); From 10d500525a048869a9f6322b0e3571c245cc5fe8 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 16:16:49 -0700 Subject: [PATCH 017/588] Make cell be undeletable ONLY when metadata is explicitly false --- IPython/html/static/notebook/js/cell.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/IPython/html/static/notebook/js/cell.js b/IPython/html/static/notebook/js/cell.js index d764ab786..e0a81655f 100644 --- a/IPython/html/static/notebook/js/cell.js +++ b/IPython/html/static/notebook/js/cell.js @@ -422,14 +422,17 @@ define([ }; /** - * is the cell deletable? (true by default) + * 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 === undefined) { - return true; + if (this.metadata.deletable === false) { + return false; } - return Boolean(this.metadata.deletable); + return true; }; /** From 756d4063c3fe648e9fa4ac8c3da0afac99d93c09 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 16:23:04 -0700 Subject: [PATCH 018/588] Fix tests --- IPython/html/tests/notebook/deletecell.js | 33 ++++++++--------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/IPython/html/tests/notebook/deletecell.js b/IPython/html/tests/notebook/deletecell.js index 477a8bddc..6c98f28cc 100644 --- a/IPython/html/tests/notebook/deletecell.js +++ b/IPython/html/tests/notebook/deletecell.js @@ -21,7 +21,7 @@ casper.notebook_test(function () { this.thenEvaluate(function() { IPython.notebook.get_cell(1).metadata.deletable = false; - IPython.notebook.get_cell(2).metadata.deletable = 0; + IPython.notebook.get_cell(2).metadata.deletable = 0; // deletable only when exactly false IPython.notebook.get_cell(3).metadata.deletable = true; }); @@ -29,7 +29,7 @@ casper.notebook_test(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 not deletable'); + this.test.assert(cell_is_deletable(2), 'Cell 2 is deletable'); this.test.assert(cell_is_deletable(3), 'Cell 3 is deletable'); }); @@ -56,44 +56,33 @@ casper.notebook_test(function () { this.validate_notebook_state('dd', 'command', 0); }); - // Try to delete cell 1 (should fail) + // 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(), 3, 'Delete cell 1: There are still 3 cells'); + 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), b, 'Delete cell 1: Cell 1 was not deleted'); - this.test.assertEquals(this.get_cell_text(2), c, 'Delete cell 1: Cell 2 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 2 (should succeed) + // Try to delete cell 1 (should succeed) this.then(function () { - this.select_cell(2); + this.select_cell(1); this.trigger_keydown('d', 'd'); - this.test.assertEquals(this.get_cells_length(), 2, 'Delete cell 2: There are now 2 cells'); + 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.test.assertEquals(this.get_cell_text(1), b, 'Delete cell 2: Cell 1 was not affected'); - this.validate_notebook_state('dd', 'command', 1); + this.validate_notebook_state('dd', 'command', 0); }); - // Change the deletable status of the last two cells + // Change the deletable status of the last cells this.thenEvaluate(function() { IPython.notebook.get_cell(0).metadata.deletable = true; - IPython.notebook.get_cell(1).metadata.deletable = true; }); this.then(function () { - // Check deletable status of the cells + // Check deletable status of the cell this.test.assert(cell_is_deletable(0), 'Cell 0 is deletable'); - this.test.assert(cell_is_deletable(1), 'Cell 1 is deletable'); - - // Try to delete cell 1 (should succeed) - 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 1: Cell 0 was not affected'); - this.validate_notebook_state('dd', 'command', 0); // Try to delete the last cell (should succeed) this.select_cell(0); From 180dd71e8154d9a5415a7d7d11f4e8da934ed6a2 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 11:08:27 -0700 Subject: [PATCH 019/588] Allow timeout and click callback --- .../static/notebook/js/notificationwidget.js | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/IPython/html/static/notebook/js/notificationwidget.js b/IPython/html/static/notebook/js/notificationwidget.js index d4ed891af..e08013b7c 100644 --- a/IPython/html/static/notebook/js/notificationwidget.js +++ b/IPython/html/static/notebook/js/notificationwidget.js @@ -16,11 +16,8 @@ define([ this.style(); } this.element.hide(); - var that = this; - this.inner = $(''); this.element.append(this.inner); - }; NotificationWidget.prototype.style = function () { @@ -34,9 +31,8 @@ define([ // click_callback : function called if user click on notification // could return false to prevent the notification to be dismissed NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) { - var options = options || {}; - var callback = click_callback || function() {return true;}; - var that = this; + options = options || {}; + // unbind potential previous callback this.element.unbind('click'); this.inner.attr('class', options.icon); @@ -48,50 +44,58 @@ define([ this.element.removeClass(); this.style(); if (options.class){ - - this.element.addClass(options.class) + this.element.addClass(options.class); } + + // clear previous timer if (this.timeout !== null) { clearTimeout(this.timeout); this.timeout = null; } - if (timeout !== undefined && timeout >=0) { + + // set the timer if a timeout is given + var that = this; + if (timeout !== undefined && timeout >= 0) { this.timeout = setTimeout(function () { that.element.fadeOut(100, function () {that.inner.text('');}); + that.element.unbind('click'); that.timeout = null; }, timeout); - } else { + } + + // bind the click callback if it is given + if (click_callback !== undefined) { this.element.click(function() { - if( callback() !== false ) { + if (click_callback() !== false) { that.element.fadeOut(100, function () {that.inner.text('');}); that.element.unbind('click'); } - if (that.timeout !== undefined) { - that.timeout = undefined; + if (that.timeout !== null) { clearTimeout(that.timeout); + that.timeout = null; } }); } }; - NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) { - var options = options || {}; - options.class = options.class +' info'; - var timeout = timeout || 3500; + options = options || {}; + options.class = options.class + ' info'; + timeout = timeout || 3500; this.set_message(msg, timeout, click_callback, options); - } + }; + NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) { - var options = options || {}; - options.class = options.class +' warning'; + options = options || {}; + options.class = options.class + ' warning'; this.set_message(msg, timeout, click_callback, options); - } + }; + NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) { - var options = options || {}; - options.class = options.class +' danger'; + options = options || {}; + options.class = options.class + ' danger'; this.set_message(msg, timeout, click_callback, options); - } - + }; NotificationWidget.prototype.get_message = function () { return this.inner.html(); From 5769a5bd97ad2b952168af211042e6f3c8b932bb Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 11:36:32 -0700 Subject: [PATCH 020/588] Add documentation to NotificationWidget methods --- .../static/notebook/js/notificationwidget.js | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/IPython/html/static/notebook/js/notificationwidget.js b/IPython/html/static/notebook/js/notificationwidget.js index e08013b7c..b95848095 100644 --- a/IPython/html/static/notebook/js/notificationwidget.js +++ b/IPython/html/static/notebook/js/notificationwidget.js @@ -20,16 +20,34 @@ define([ this.element.append(this.inner); }; + /** + * Add the 'notification_widget' CSS class to the widget element. + * + * @method style + */ NotificationWidget.prototype.style = function () { this.element.addClass('notification_widget'); }; - // msg : message to display - // timeout : time in ms before diseapearing - // - // if timeout <= 0 - // click_callback : function called if user click on notification - // could return false to prevent the notification to be dismissed + /** + * Set the notification widget message to display for a certain + * amount of time (timeout). The widget will be shown forever if + * timeout is <= 0 or undefined. If the widget is clicked while it + * is still displayed, execute an optional callback + * (click_callback). If the callback returns false, it will + * prevent the notification from being dismissed. + * + * Options: + * class - CSS class name for styling + * icon - CSS class name for the widget icon + * title - HTML title attribute for the widget + * + * @method set_message + * @param {string} msg - The notification to display + * @param {integer} [timeout] - The amount of time in milliseconds to display the widget + * @param {function} [click_callback] - The function to run when the widget is clicked + * @param {Object} [options] - Additional options + */ NotificationWidget.prototype.set_message = function (msg, timeout, click_callback, options) { options = options || {}; @@ -43,7 +61,7 @@ define([ // reset previous set style this.element.removeClass(); this.style(); - if (options.class){ + if (options.class) { this.element.addClass(options.class); } @@ -68,8 +86,8 @@ define([ this.element.click(function() { if (click_callback() !== false) { that.element.fadeOut(100, function () {that.inner.text('');}); - that.element.unbind('click'); } + that.element.unbind('click'); if (that.timeout !== null) { clearTimeout(that.timeout); that.timeout = null; @@ -78,6 +96,12 @@ define([ } }; + /** + * Display an information message (styled with the 'info' + * class). Arguments are the same as in set_message. + * + * @method info + */ NotificationWidget.prototype.info = function (msg, timeout, click_callback, options) { options = options || {}; options.class = options.class + ' info'; @@ -85,18 +109,36 @@ define([ this.set_message(msg, timeout, click_callback, options); }; + /** + * Display a warning message (styled with the 'warning' + * class). Arguments are the same as in set_message. + * + * @method warning + */ NotificationWidget.prototype.warning = function (msg, timeout, click_callback, options) { options = options || {}; options.class = options.class + ' warning'; this.set_message(msg, timeout, click_callback, options); }; + /** + * Display a danger message (styled with the 'danger' + * class). Arguments are the same as in set_message. + * + * @method danger + */ NotificationWidget.prototype.danger = function (msg, timeout, click_callback, options) { options = options || {}; options.class = options.class + ' danger'; this.set_message(msg, timeout, click_callback, options); }; + /** + * Get the text of the widget message. + * + * @method get_message + * @return {string} - the message text + */ NotificationWidget.prototype.get_message = function () { return this.inner.html(); }; From 2c5a10a6e040fa6ce0f7c813505b5e7e68a11357 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 12:10:55 -0700 Subject: [PATCH 021/588] Add documentation to notification area --- .../static/notebook/js/notificationarea.js | 106 ++++++++++++------ .../static/notebook/js/notificationwidget.js | 7 ++ 2 files changed, 77 insertions(+), 36 deletions(-) diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index 8c406ad3b..81f650bfe 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -11,16 +11,22 @@ define([ ], function(IPython, $, utils, dialog, notificationwidget, moment) { "use strict"; + // store reference to the NotificationWidget class + var NotificationWidget = notificationwidget.NotificationWidget; + + /** + * Construct the NotificationArea object. Options are: + * events: $(Events) instance + * save_widget: SaveWidget instance + * notebook: Notebook instance + * keyboard_manager: KeyboardManager instance + * + * @constructor + * @param {string} selector - a jQuery selector string for the + * notification area element + * @param {Object} [options] - a dictionary of keyword arguments. + */ var NotificationArea = function (selector, options) { - // Constructor - // - // Parameters: - // selector: string - // options: dictionary - // Dictionary of keyword arguments. - // notebook: Notebook instance - // events: $(Events) instance - // save_widget: SaveWidget instance this.selector = selector; this.events = options.events; this.save_widget = options.save_widget; @@ -32,47 +38,70 @@ define([ this.widget_dict = {}; }; - NotificationArea.prototype.temp_message = function (msg, timeout, css_class) { - var tdiv = $('
') - .addClass('notification_widget') - .addClass(css_class) - .hide() - .text(msg); - - $(this.selector).append(tdiv); - var tmout = Math.max(1500,(timeout||1500)); - tdiv.fadeIn(100); - - setTimeout(function () { - tdiv.fadeOut(100, function () {tdiv.remove();}); - }, tmout); - }; - - NotificationArea.prototype.widget = function(name) { - if(this.widget_dict[name] === undefined) { + /** + * Get a widget by name, creating it if it doesn't exist. + * + * @method widget + * @param {string} name - the widget name + */ + NotificationArea.prototype.widget = function (name) { + if (this.widget_dict[name] === undefined) { return this.new_notification_widget(name); } return this.get_widget(name); }; - NotificationArea.prototype.get_widget = function(name) { + /** + * Get a widget by name, throwing an error if it doesn't exist. + * + * @method get_widget + * @param {string} name - the widget name + */ + NotificationArea.prototype.get_widget = function (name) { if(this.widget_dict[name] === undefined) { throw('no widgets with this name'); } return this.widget_dict[name]; }; - NotificationArea.prototype.new_notification_widget = function(name) { - if(this.widget_dict[name] !== undefined) { - throw('widget with that name already exists ! '); + /** + * Create a new notification widget with the given name. The + * widget must not already exist. + * + * @method new_notification_widget + * @param {string} name - the widget name + */ + NotificationArea.prototype.new_notification_widget = function (name) { + if (this.widget_dict[name] !== undefined) { + throw('widget with that name already exists!'); } - var div = $('
').attr('id','notification_'+name); + + // create the element for the notification widget and add it + // to the notification aread element + var div = $('
').attr('id', 'notification_' + name); $(this.selector).append(div); - this.widget_dict[name] = new notificationwidget.NotificationWidget('#notification_'+name); + + // create the widget object and return it + this.widget_dict[name] = new NotificationWidget('#notification_' + name); return this.widget_dict[name]; }; - NotificationArea.prototype.init_notification_widgets = function() { + /** + * Initialize the default set of notification widgets. + * + * @method init_notification_widgets + */ + NotificationArea.prototype.init_notification_widgets = function () { + this.init_kernel_notification_widget(); + this.init_notebook_notification_widget(); + }; + + /** + * Initialize the notification widget for kernel status messages. + * + * @method init_kernel_notification_widget + */ + NotificationArea.prototype.init_kernel_notification_widget = function () { var that = this; var knw = this.new_notification_widget('kernel'); var $kernel_ind_icon = $("#kernel_indicator_icon"); @@ -194,8 +223,14 @@ define([ } }); }); + }; - + /** + * Initialize the notification widget for notebook status messages. + * + * @method init_notebook_notification_widget + */ + NotificationArea.prototype.init_notebook_notification_widget = function () { var nnw = this.new_notification_widget('notebook'); // Notebook events @@ -247,7 +282,6 @@ define([ this.events.on('autosave_enabled.Notebook', function (evt, interval) { nnw.set_message("Saving every " + interval / 1000 + "s", 1000); }); - }; IPython.NotificationArea = NotificationArea; diff --git a/IPython/html/static/notebook/js/notificationwidget.js b/IPython/html/static/notebook/js/notificationwidget.js index b95848095..b148cb427 100644 --- a/IPython/html/static/notebook/js/notificationwidget.js +++ b/IPython/html/static/notebook/js/notificationwidget.js @@ -7,6 +7,13 @@ define([ ], function(IPython, $) { "use strict"; + /** + * Construct a NotificationWidget object. + * + * @constructor + * @param {string} selector - a jQuery selector string for the + * notification widget element + */ var NotificationWidget = function (selector) { this.selector = selector; this.timeout = null; From e68f6e585aec10b143587de769cb5c22b02e50c7 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 16:03:24 -0700 Subject: [PATCH 022/588] Small changes to notification widget --- IPython/html/static/notebook/js/notificationwidget.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/notebook/js/notificationwidget.js b/IPython/html/static/notebook/js/notificationwidget.js index b148cb427..a3688f7db 100644 --- a/IPython/html/static/notebook/js/notificationwidget.js +++ b/IPython/html/static/notebook/js/notificationwidget.js @@ -90,7 +90,7 @@ define([ // bind the click callback if it is given if (click_callback !== undefined) { - this.element.click(function() { + this.element.click(function () { if (click_callback() !== false) { that.element.fadeOut(100, function () {that.inner.text('');}); } @@ -105,7 +105,8 @@ define([ /** * Display an information message (styled with the 'info' - * class). Arguments are the same as in set_message. + * class). Arguments are the same as in set_message. Default + * timeout is 3500 milliseconds. * * @method info */ From 6f49f4b78ec0ca4333f5c2554c047c720d035390 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 25 Sep 2014 16:03:44 -0700 Subject: [PATCH 023/588] Add tests for notification area and widgets --- IPython/html/tests/notebook/notifications.js | 116 +++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 IPython/html/tests/notebook/notifications.js diff --git a/IPython/html/tests/notebook/notifications.js b/IPython/html/tests/notebook/notifications.js new file mode 100644 index 000000000..7366930d2 --- /dev/null +++ b/IPython/html/tests/notebook/notifications.js @@ -0,0 +1,116 @@ +// Test the notification area and widgets + +casper.notebook_test(function () { + var that = this; + var widget = function (name) { + return that.evaluate(function (name) { + return (IPython.notification_area.widget(name) !== undefined); + }, name); + }; + + var get_widget = function (name) { + return that.evaluate(function (name) { + return (IPython.notification_area.get_widget(name) !== undefined); + }, name); + }; + + var new_notification_widget = function (name) { + return that.evaluate(function (name) { + return (IPython.notification_area.new_notification_widget(name) !== undefined); + }, name); + }; + + var widget_has_class = function (name, class_name) { + return that.evaluate(function (name, class_name) { + var w = IPython.notification_area.get_widget(name); + return w.element.hasClass(class_name); + }, name, class_name); + }; + + var widget_message = function (name) { + return that.evaluate(function (name) { + var w = IPython.notification_area.get_widget(name); + return w.get_message(); + }, name); + }; + + this.then(function () { + // check that existing widgets are there + this.test.assert(get_widget('kernel') && widget('kernel'), 'The kernel notification widget exists'); + this.test.assert(get_widget('notebook') && widget('notbook'), 'The notebook notification widget exists'); + + // try getting a non-existant widget + this.test.assertRaises(get_widget, 'foo', 'get_widget: error is thrown'); + + // try creating a non-existant widget + this.test.assert(widget('bar'), 'widget: new widget is created'); + + // try creating a widget that already exists + this.test.assertRaises(new_notification_widget, 'kernel', 'new_notification_widget: error is thrown'); + }); + + // test creating 'info' messages + this.thenEvaluate(function () { + var tnw = IPython.notification_area.widget('test'); + tnw.info('test info'); + }); + this.waitUntilVisible('#notification_test', function () { + this.test.assert(widget_has_class('test', 'info'), 'info: class is correct'); + this.test.assertEquals(widget_message('test'), 'test info', 'info: message is correct'); + }); + + // test creating 'warning' messages + this.thenEvaluate(function () { + var tnw = IPython.notification_area.widget('test'); + tnw.warning('test warning'); + }); + this.waitUntilVisible('#notification_test', function () { + this.test.assert(widget_has_class('test', 'warning'), 'warning: class is correct'); + this.test.assertEquals(widget_message('test'), 'test warning', 'warning: message is correct'); + }); + + // test creating 'danger' messages + this.thenEvaluate(function () { + var tnw = IPython.notification_area.widget('test'); + tnw.danger('test danger'); + }); + this.waitUntilVisible('#notification_test', function () { + this.test.assert(widget_has_class('test', 'danger'), 'danger: class is correct'); + this.test.assertEquals(widget_message('test'), 'test danger', 'danger: message is correct'); + }); + + // test message timeout + this.thenEvaluate(function () { + var tnw = IPython.notification_area.widget('test'); + tnw.set_message('test timeout', 1000); + }); + this.waitUntilVisible('#notification_test', function () { + this.test.assertEquals(widget_message('test'), 'test timeout', 'timeout: message is correct'); + }); + this.waitWhileVisible('#notification_test', function () { + this.test.assertEquals(widget_message('test'), '', 'timeout: message was cleared'); + }); + + // test click callback + this.thenEvaluate(function () { + var tnw = IPython.notification_area.widget('test'); + tnw._clicked = false; + tnw.set_message('test click', undefined, function () { + tnw._clicked = true; + return true; + }); + }); + this.waitUntilVisible('#notification_test', function () { + this.test.assertEquals(widget_message('test'), 'test click', 'callback: message is correct'); + this.click('#notification_test'); + }); + this.waitFor(function () { + return this.evaluate(function () { + return IPython.notification_area.widget('test')._clicked; + }); + }, function () { + this.waitWhileVisible('#notification_test', function () { + this.test.assertEquals(widget_message('test'), '', 'callback: message was cleared'); + }); + }); +}); From b6a0f60d3f3187b8a7e1a8a39b01439ad05e60f5 Mon Sep 17 00:00:00 2001 From: Sylvain Corlay Date: Fri, 26 Sep 2014 01:50:04 +0000 Subject: [PATCH 024/588] Make Widget.views be an object indexed by view id --- IPython/html/static/widgets/js/widget.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js index 4345a411f..b19976b36 100644 --- a/IPython/html/static/widgets/js/widget.js +++ b/IPython/html/static/widgets/js/widget.js @@ -26,7 +26,7 @@ define(["widgets/js/manager", this.msg_buffer = null; this.state_lock = null; this.id = model_id; - this.views = []; + this.views = {}; if (comm !== undefined) { // Remember comm associated with the model. @@ -57,9 +57,9 @@ define(["widgets/js/manager", delete this.comm.model; // Delete ref so GC will collect widget model. delete this.comm; delete this.model_id; // Delete id from model so widget manager cleans up. - _.each(this.views, function(view, i) { - view.remove(); - }); + for (var id in this.views) { + this.views[id].remove(); + } }, _handle_comm_msg: function (msg) { @@ -293,8 +293,8 @@ define(["widgets/js/manager", this.options = parameters.options; this.child_model_views = {}; this.child_views = {}; - this.model.views.push(this); this.id = this.id || IPython.utils.uuid(); + this.model.views[this.id] = this; this.on('displayed', function() { this.is_displayed = true; }, this); @@ -339,7 +339,7 @@ define(["widgets/js/manager", var view = this.child_views[view_id]; delete this.child_views[view_id]; view_ids.splice(0,1); - child_model.views.pop(view); + delete child_model.views[view_id]; // Remove the view list specific to this model if it is empty. if (view_ids.length === 0) { From 9c1e7fa2ebaf3a3b7bbfda1f172331ae78fe5c9e Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Fri, 26 Sep 2014 00:12:24 -0700 Subject: [PATCH 025/588] Clarify stickiness of warning/danger notifications --- IPython/html/static/notebook/js/notificationwidget.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/notebook/js/notificationwidget.js b/IPython/html/static/notebook/js/notificationwidget.js index a3688f7db..0c7f6720c 100644 --- a/IPython/html/static/notebook/js/notificationwidget.js +++ b/IPython/html/static/notebook/js/notificationwidget.js @@ -119,7 +119,8 @@ define([ /** * Display a warning message (styled with the 'warning' - * class). Arguments are the same as in set_message. + * class). Arguments are the same as in set_message. Messages are + * sticky by default. * * @method warning */ @@ -131,7 +132,8 @@ define([ /** * Display a danger message (styled with the 'danger' - * class). Arguments are the same as in set_message. + * class). Arguments are the same as in set_message. Messages are + * sticky by default. * * @method danger */ From cb005e916c256759eed9a10b902f63b4eb2b6bb8 Mon Sep 17 00:00:00 2001 From: MinRK Date: Thu, 25 Sep 2014 16:03:59 -0700 Subject: [PATCH 026/588] run iptest in Dockerfile and install sphinx with apt, since it's super slow because of 2to3 --- Dockerfile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index d511df1f5..445eba2d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ FROM ubuntu:14.04 MAINTAINER IPython Project +ENV DEBIAN_FRONTEND noninteractive + # Make sure apt is up to date RUN apt-get update RUN apt-get upgrade -y @@ -27,7 +29,7 @@ RUN apt-get install -y -q build-essential make gcc zlib1g-dev git && \ # In order to build from source, need less RUN npm install -g less -RUN apt-get -y install fabric +RUN apt-get install -y -q fabric python-sphinx python3-sphinx RUN mkdir -p /srv/ WORKDIR /srv/ @@ -37,10 +39,14 @@ RUN chmod -R +rX /srv/ipython # .[all] only works with -e, so use file://path#egg # Can't use -e because ipython2 and ipython3 will clobber each other -RUN pip2 install --upgrade file:///srv/ipython#egg=ipython[all] -RUN pip3 install --upgrade file:///srv/ipython#egg=ipython[all] +RUN pip2 install file:///srv/ipython#egg=ipython[all] +RUN pip3 install file:///srv/ipython#egg=ipython[all] # install kernels RUN python2 -m IPython kernelspec install-self --system RUN python3 -m IPython kernelspec install-self --system +WORKDIR /tmp/ + +RUN iptest2 +RUN iptest3 From 3997769defff4ae3041827497d5a8ada4662de80 Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Sat, 27 Sep 2014 15:57:01 -0400 Subject: [PATCH 027/588] Fixed off by one error in get_prev_cell Not sure why this was a TODO. Maybe `find_cell_index()` returned zero at one time, but in the browsers I tested, it always returns null if not found. --- IPython/html/static/notebook/js/notebook.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index ac8b220a3..522216881 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -465,11 +465,9 @@ define([ * @return {Cell} The previous cell */ Notebook.prototype.get_prev_cell = function (cell) { - // TODO: off-by-one - // nb.get_prev_cell(nb.get_cell(1)) is null var result = null; var index = this.find_cell_index(cell); - if (index !== null && index > 1) { + if (index !== null && index > 0) { result = this.get_cell(index-1); } return result; From 974d45343e9e3ae498c1b33ba23817360cd8df57 Mon Sep 17 00:00:00 2001 From: MinRK Date: Sat, 27 Sep 2014 15:16:42 -0700 Subject: [PATCH 028/588] allow kernel_name to be undefined in requests fallback to KM.default_kernel_name in that case --- IPython/html/services/sessions/handlers.py | 3 ++- .../html/services/sessions/sessionmanager.py | 24 ++++--------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index 691339f0d..98def21a5 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -45,7 +45,8 @@ class SessionRootHandler(IPythonHandler): try: kernel_name = model['kernel']['name'] except KeyError: - raise web.HTTPError(400, "Missing field in JSON data: kernel.name") + self.log.debug("No kernel name specified, using default kernel") + kernel_name = None # Check to see if session exists if sm.session_exists(name=name, path=path): diff --git a/IPython/html/services/sessions/sessionmanager.py b/IPython/html/services/sessions/sessionmanager.py index b105344c1..fc1674b3c 100644 --- a/IPython/html/services/sessions/sessionmanager.py +++ b/IPython/html/services/sessions/sessionmanager.py @@ -1,20 +1,7 @@ -"""A base class session manager. +"""A base class session manager.""" -Authors: - -* Zach Sailer -""" - -#----------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import uuid import sqlite3 @@ -25,9 +12,6 @@ from IPython.config.configurable import LoggingConfigurable from IPython.utils.py3compat import unicode_type from IPython.utils.traitlets import Instance -#----------------------------------------------------------------------------- -# Classes -#----------------------------------------------------------------------------- class SessionManager(LoggingConfigurable): @@ -73,7 +57,7 @@ class SessionManager(LoggingConfigurable): "Create a uuid for a new session" return unicode_type(uuid.uuid4()) - def create_session(self, name=None, path=None, kernel_name='python'): + def create_session(self, name=None, path=None, kernel_name=None): """Creates a session and returns its model""" session_id = self.new_session_id() # allow nbm to specify kernels cwd From 6c3d40bbcd438d974c8b4356f2453738de7abb9a Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Sun, 28 Sep 2014 14:01:35 -0400 Subject: [PATCH 029/588] Update documentation for functions that can return null --- IPython/html/static/notebook/js/notebook.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 522216881..7e17848da 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -430,7 +430,7 @@ define([ * * @method get_cell * @param {Number} index An index of a cell to retrieve - * @return {Cell} A particular cell + * @return {Cell} Cell or null if no cell was found. */ Notebook.prototype.get_cell = function (index) { var result = null; @@ -446,7 +446,7 @@ define([ * * @method get_next_cell * @param {Cell} cell The provided cell - * @return {Cell} The next cell + * @return {Cell} the next cell or null if no cell was found. */ Notebook.prototype.get_next_cell = function (cell) { var result = null; @@ -462,7 +462,7 @@ define([ * * @method get_prev_cell * @param {Cell} cell The provided cell - * @return {Cell} The previous cell + * @return {Cell} The previous cell or null if no cell was found. */ Notebook.prototype.get_prev_cell = function (cell) { var result = null; @@ -478,7 +478,7 @@ define([ * * @method find_cell_index * @param {Cell} cell The provided cell - * @return {Number} The cell's numeric index + * @return {Number} The cell's numeric index or null if no cell was found. */ Notebook.prototype.find_cell_index = function (cell) { var result = null; From caddaec6181af447133c7fcb9a0d61b24c6266c3 Mon Sep 17 00:00:00 2001 From: MinRK Date: Sat, 27 Sep 2014 20:16:23 -0700 Subject: [PATCH 030/588] remove references to kernel config in parent config files since it doesn't work anymore. Link to the kernel config doc instead. --- docs/source/notebook/public_server.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/source/notebook/public_server.rst b/docs/source/notebook/public_server.rst index 1745497e6..f79d1799e 100644 --- a/docs/source/notebook/public_server.rst +++ b/docs/source/notebook/public_server.rst @@ -95,9 +95,6 @@ commented; the minimum set you need to uncomment and edit is the following:: c = get_config() - # Kernel config - c.IPKernelApp.pylab = 'inline' # if you want plotting support always - # Notebook config c.NotebookApp.certfile = u'/absolute/path/to/your/certificate/mycert.pem' c.NotebookApp.ip = '*' From 072bcdc4842854c953e4d07be2258e47fb253548 Mon Sep 17 00:00:00 2001 From: MinRK Date: Sat, 27 Sep 2014 15:16:42 -0700 Subject: [PATCH 031/588] allow kernel_name to be undefined in js Falls back to KM.default_kernel_name, as configured server-side. --- IPython/html/static/notebook/js/kernelselector.js | 2 +- IPython/html/static/notebook/js/notebook.js | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/IPython/html/static/notebook/js/kernelselector.js b/IPython/html/static/notebook/js/kernelselector.js index ce9b91960..759b97e3c 100644 --- a/IPython/html/static/notebook/js/kernelselector.js +++ b/IPython/html/static/notebook/js/kernelselector.js @@ -12,7 +12,7 @@ define([ this.selector = selector; this.notebook = notebook; this.events = notebook.events; - this.current_selection = notebook.default_kernel_name; + this.current_selection = null; this.kernelspecs = {}; if (this.selector !== undefined) { this.element = $(selector); diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index ac8b220a3..6603fa3e7 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -70,9 +70,6 @@ define([ // Create default scroll manager. this.scroll_manager = new scrollmanager.ScrollManager(this); - // default_kernel_name is a temporary measure while we implement proper - // kernel selection and delayed start. Do not rely on it. - this.default_kernel_name = 'python'; // TODO: This code smells (and the other `= this` line a couple lines down) // We need a better way to deal with circular instance references. this.keyboard_manager.notebook = this; @@ -1565,9 +1562,6 @@ define([ */ Notebook.prototype.start_session = function (kernel_name) { var that = this; - if (kernel_name === undefined) { - kernel_name = this.default_kernel_name; - } if (this._session_starting) { throw new session.SessionAlreadyStarting(); } @@ -2332,7 +2326,7 @@ define([ // code execution upon loading, which is a security risk. if (this.session === null) { var kernelspec = this.metadata.kernelspec || {}; - var kernel_name = kernelspec.name || this.default_kernel_name; + var kernel_name = kernelspec.name; this.start_session(kernel_name); } From 60925f0a1abb4fa732129b19f3ae3891d4820ae4 Mon Sep 17 00:00:00 2001 From: "sylvain.corlay" Date: Sun, 28 Sep 2014 23:40:27 -0400 Subject: [PATCH 032/588] hasOwnProperty --- IPython/html/static/widgets/js/widget.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js index b19976b36..e2f1bed73 100644 --- a/IPython/html/static/widgets/js/widget.js +++ b/IPython/html/static/widgets/js/widget.js @@ -58,7 +58,9 @@ define(["widgets/js/manager", delete this.comm; delete this.model_id; // Delete id from model so widget manager cleans up. for (var id in this.views) { - this.views[id].remove(); + if (this.views.hasOwnProperty(id)) { + this.views[id].remove(); + } } }, From 9155675440f5a15692d4eed325913db752dded74 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 28 Sep 2014 18:18:28 +0200 Subject: [PATCH 033/588] drop more 2.6 hacks --- IPython/html/notebookapp.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index b1b5c844d..d0a71e960 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -137,15 +137,7 @@ class NotebookWebApplication(web.Application): cluster_manager, session_manager, kernel_spec_manager, log, base_url, default_url, settings_overrides, jinja_env_options=None): - # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and - # base_url will always be unicode, which will in turn - # make the patterns unicode, and ultimately result in unicode - # keys in kwargs to handler._execute(**kwargs) in tornado. - # This enforces that base_url be ascii in that situation. - # - # Note that the URLs these patterns check against are escaped, - # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'. - base_url = py3compat.unicode_to_str(base_url, 'ascii') + _template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates")) if isinstance(_template_path, str): _template_path = (_template_path,) From be4b180b0318a793be14586169977284bddd211a Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sun, 28 Sep 2014 15:20:04 +0200 Subject: [PATCH 034/588] cast unicode to allow json dump --- setupbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setupbase.py b/setupbase.py index f2f20d14c..d0495beb3 100644 --- a/setupbase.py +++ b/setupbase.py @@ -579,7 +579,7 @@ def git_prebuild(pkg_dir, build_cmd=build_py): with open(out_pth, 'w') as out_file: out_file.writelines([ '# GENERATED BY setup.py\n', - 'commit = "%s"\n' % repo_commit, + 'commit = u"%s"\n' % repo_commit, ]) return require_submodules(MyBuildPy) From a060056e369c6aac2fd64ce7224c129d25ccfabb Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 29 Sep 2014 14:28:29 -0700 Subject: [PATCH 035/588] remove unused dateformat we are using moment.js for dates now --- IPython/html/templates/page.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/IPython/html/templates/page.html b/IPython/html/templates/page.html index ea0a89d44..9d6bf4edc 100644 --- a/IPython/html/templates/page.html +++ b/IPython/html/templates/page.html @@ -25,7 +25,6 @@ jquery: 'components/jquery/jquery.min', bootstrap: 'components/bootstrap/js/bootstrap.min', bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min', - dateformat: 'dateformat/date.format', jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min', highlight: 'components/highlight.js/build/highlight.pack', moment: "components/moment/moment", @@ -46,9 +45,6 @@ deps: ["bootstrap"], exports: "Tour" }, - dateformat: { - exports: "dateFormat" - }, jqueryui: { deps: ["jquery"], exports: "$" From cfc234dc89d7f3490bd33a3ccbc5b2546a0f40ed Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Fri, 26 Sep 2014 17:41:21 -0700 Subject: [PATCH 036/588] Handle NoSuchKernel errors more gracefully --- IPython/html/services/sessions/handlers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index 98def21a5..603dabcc5 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -10,6 +10,7 @@ from tornado import web from ...base.handlers import IPythonHandler, json_errors from IPython.utils.jsonutil import date_default from IPython.html.utils import url_path_join, url_escape +from IPython.kernel.kernelspec import NoSuchKernel class SessionRootHandler(IPythonHandler): @@ -52,7 +53,11 @@ class SessionRootHandler(IPythonHandler): if sm.session_exists(name=name, path=path): model = sm.get_session(name=name, path=path) else: - model = sm.create_session(name=name, path=path, kernel_name=kernel_name) + try: + model = sm.create_session(name=name, path=path, kernel_name=kernel_name) + except NoSuchKernel: + raise web.HTTPError(400, "No such kernel: %s" % kernel_name) + location = url_path_join(self.base_url, 'api', 'sessions', model['id']) self.set_header('Location', url_escape(location)) self.set_status(201) From 58fcb3abb9ec2b2287e078479ad8d1aa542a8600 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Fri, 26 Sep 2014 17:41:45 -0700 Subject: [PATCH 037/588] Show the user a different notification --- .../static/notebook/js/notificationarea.js | 19 ++++++++++++++++++- .../html/static/services/kernels/js/kernel.js | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index 81f650bfe..5c231c283 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -159,7 +159,7 @@ define([ }); }); - this.events.on('status_dead.Kernel',function () { + this.events.on('status_restart_failed.Kernel',function () { var msg = 'The kernel has died, and the automatic restart has failed.' + ' It is possible the kernel cannot be restarted.' + ' If you are not able to restart the kernel, you will still be able to save' + @@ -184,6 +184,23 @@ define([ }); }); + this.events.on('start_failed.Session',function () { + var msg = 'We were unable to start the kernel. This might ' + + 'happen if the notebook was previously run with a kernel ' + + 'that you do not have installed. Please choose a different kernel, ' + + 'or install the needed kernel and then refresh this page.'; + + dialog.modal({ + title: "Failed to start the kernel", + body : msg, + keyboard_manager: that.keyboard_manager, + notebook: that.notebook, + buttons : { + "Ok": { class: 'btn-primary' } + } + }); + }); + this.events.on('websocket_closed.Kernel', function (event, data) { var kernel = data.kernel; var ws_url = data.ws_url; diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index 9b4937a22..e1ec7e4e9 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -560,6 +560,7 @@ define([ } else if (execution_state === 'dead') { this.stop_channels(); this.events.trigger('status_dead.Kernel', {kernel: this}); + this.events.trigger('status_restart_failed.Kernel', {kernel: this}); } }; From 263181c4164bf9f5545d9f88bd82eae49aefa266 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Fri, 26 Sep 2014 18:40:14 -0700 Subject: [PATCH 038/588] Report the exact error that occurred --- .../static/notebook/js/notificationarea.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index 5c231c283..feef3da0b 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -184,11 +184,23 @@ define([ }); }); - this.events.on('start_failed.Session',function () { - var msg = 'We were unable to start the kernel. This might ' + - 'happen if the notebook was previously run with a kernel ' + - 'that you do not have installed. Please choose a different kernel, ' + - 'or install the needed kernel and then refresh this page.'; + this.events.on('start_failed.Session',function (session, xhr, status, error) { + var msg = $('
'); + msg.append($('
') + .text('We were unable to start the kernel. This might ' + + 'happen if the notebook was previously run with a kernel ' + + 'that you do not have installed. Please choose a different kernel, ' + + 'or install the needed kernel and then refresh this page.') + .css('margin-bottom', '1em')); + + msg.append($('
') + .text('The exact error was:') + .css('margin-bottom', '1em')); + + msg.append($('
') + .attr('class', 'alert alert-danger') + .attr('role', 'alert') + .text(JSON.parse(status.responseText).message)); dialog.modal({ title: "Failed to start the kernel", From 5ba858fc7caa1df5adeace77335ed4be62b55568 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Fri, 26 Sep 2014 18:58:09 -0700 Subject: [PATCH 039/588] Remove 'we' from message --- IPython/html/static/notebook/js/notificationarea.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index feef3da0b..c2f9837ee 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -187,7 +187,7 @@ define([ this.events.on('start_failed.Session',function (session, xhr, status, error) { var msg = $('
'); msg.append($('
') - .text('We were unable to start the kernel. This might ' + + .text('The kernel could not be started. This might ' + 'happen if the notebook was previously run with a kernel ' + 'that you do not have installed. Please choose a different kernel, ' + 'or install the needed kernel and then refresh this page.') From c4a89cd54dd365e383a6a0ef568d3648797f92a5 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Fri, 26 Sep 2014 19:22:30 -0700 Subject: [PATCH 040/588] Better user experience when kernel isn't found --- IPython/html/services/sessions/handlers.py | 6 ++- .../static/notebook/js/notificationarea.js | 38 +++++++------------ .../static/services/sessions/js/session.js | 1 - 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index 603dabcc5..8b4a6d3e6 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -56,7 +56,11 @@ class SessionRootHandler(IPythonHandler): try: model = sm.create_session(name=name, path=path, kernel_name=kernel_name) except NoSuchKernel: - raise web.HTTPError(400, "No such kernel: %s" % kernel_name) + msg = ("The '%s' kernel is not available. Please pick another " + "suitable kernel instead, or install that kernel." % kernel_name) + status_msg = 'Kernel not found' + msg = dict(full=msg, short=status_msg) + raise web.HTTPError(400, json.dumps(msg)) location = url_path_join(self.base_url, 'api', 'sessions', model['id']) self.set_header('Location', url_escape(location)) diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index c2f9837ee..a87e3e8cc 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -185,31 +185,21 @@ define([ }); this.events.on('start_failed.Session',function (session, xhr, status, error) { - var msg = $('
'); - msg.append($('
') - .text('The kernel could not be started. This might ' + - 'happen if the notebook was previously run with a kernel ' + - 'that you do not have installed. Please choose a different kernel, ' + - 'or install the needed kernel and then refresh this page.') - .css('margin-bottom', '1em')); + var msg = JSON.parse(status.responseJSON.message); - msg.append($('
') - .text('The exact error was:') - .css('margin-bottom', '1em')); - - msg.append($('
') - .attr('class', 'alert alert-danger') - .attr('role', 'alert') - .text(JSON.parse(status.responseText).message)); - - dialog.modal({ - title: "Failed to start the kernel", - body : msg, - keyboard_manager: that.keyboard_manager, - notebook: that.notebook, - buttons : { - "Ok": { class: 'btn-primary' } - } + that.save_widget.update_document_title(); + $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead'); + knw.danger(msg.short, undefined, function () { + dialog.modal({ + title: "Failed to start the kernel", + body : msg.full, + keyboard_manager: that.keyboard_manager, + notebook: that.notebook, + buttons : { + "Ok": { class: 'btn-primary' } + } + }); + return false; }); }); diff --git a/IPython/html/static/services/sessions/js/session.js b/IPython/html/static/services/sessions/js/session.js index 14a93af70..d8d5b5663 100644 --- a/IPython/html/static/services/sessions/js/session.js +++ b/IPython/html/static/services/sessions/js/session.js @@ -112,7 +112,6 @@ define([ Session.prototype._handle_start_failure = function (xhr, status, error) { this.events.trigger('start_failed.Session', [this, xhr, status, error]); - this.events.trigger('status_dead.Kernel'); }; /** From 5e1e8a116c9aa294b97307a83149d49b8893bd93 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Tue, 30 Sep 2014 10:21:25 -0700 Subject: [PATCH 041/588] Use 501 error code instead of 400 --- IPython/html/services/sessions/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index 8b4a6d3e6..f38ad5859 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -60,7 +60,7 @@ class SessionRootHandler(IPythonHandler): "suitable kernel instead, or install that kernel." % kernel_name) status_msg = 'Kernel not found' msg = dict(full=msg, short=status_msg) - raise web.HTTPError(400, json.dumps(msg)) + raise web.HTTPError(501, json.dumps(msg)) location = url_path_join(self.base_url, 'api', 'sessions', model['id']) self.set_header('Location', url_escape(location)) From 46e40e5ea1490fa85b6c4b019485465b90e9e7ea Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Tue, 30 Sep 2014 10:48:43 -0700 Subject: [PATCH 042/588] Return a proper JSON object --- IPython/html/services/sessions/handlers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index f38ad5859..0573e6932 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -59,8 +59,10 @@ class SessionRootHandler(IPythonHandler): msg = ("The '%s' kernel is not available. Please pick another " "suitable kernel instead, or install that kernel." % kernel_name) status_msg = 'Kernel not found' - msg = dict(full=msg, short=status_msg) - raise web.HTTPError(501, json.dumps(msg)) + self.log.warn('Kernel not found: %s' % kernel_name) + self.set_status(501) + self.finish(json.dumps(dict(message=msg, short_message=status_msg))) + return location = url_path_join(self.base_url, 'api', 'sessions', model['id']) self.set_header('Location', url_escape(location)) From d0e942213d3f4a543e54ade31a5eaa8e67fb63f6 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Tue, 30 Sep 2014 10:48:54 -0700 Subject: [PATCH 043/588] Always show the modal dialog, and have a fallback generic message --- .../static/notebook/js/notificationarea.js | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index a87e3e8cc..f2e43a53a 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -185,14 +185,25 @@ define([ }); this.events.on('start_failed.Session',function (session, xhr, status, error) { - var msg = JSON.parse(status.responseJSON.message); + var full = status.responseJSON.message; + var short = status.responseJSON.short_message || 'Kernel error'; + var traceback = status.responseJSON.traceback; + var msg = $('
'); - that.save_widget.update_document_title(); - $kernel_ind_icon.attr('class','kernel_dead_icon').attr('title','Kernel Dead'); - knw.danger(msg.short, undefined, function () { + msg.append($('

').text(full)); + if (traceback) { + msg.append($('