From f06aca718a47c962225f717ac0ff303decc67204 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Fri, 3 Feb 2017 14:49:23 -0800 Subject: [PATCH 001/255] Wrap prompts in bdi for rtl layout --- notebook/static/notebook/js/codecell.js | 2 +- notebook/static/notebook/js/outputarea.js | 9 ++++++++- notebook/tests/notebook/prompt_numbers.js | 12 ++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/notebook/static/notebook/js/codecell.js b/notebook/static/notebook/js/codecell.js index 54781cf6f..d7dabc2a3 100644 --- a/notebook/static/notebook/js/codecell.js +++ b/notebook/static/notebook/js/codecell.js @@ -465,7 +465,7 @@ define([ } else { ns = encodeURIComponent(prompt_value); } - return 'In [' + ns + ']:'; + return 'In [' + ns + ']:'; }; CodeCell.input_prompt_continuation = function (prompt_value, lines_number) { diff --git a/notebook/static/notebook/js/outputarea.js b/notebook/static/notebook/js/outputarea.js index 4d14cf4d3..33a5ffa89 100644 --- a/notebook/static/notebook/js/outputarea.js +++ b/notebook/static/notebook/js/outputarea.js @@ -455,7 +455,14 @@ define([ var toinsert = this.create_output_area(); this._record_display_id(json, toinsert); if (this.prompt_area) { - toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:'); + toinsert.find('div.prompt') + .addClass('output_prompt') + .empty() + .append( + $('').text('Out') + ).append( + '[' + n + ']:' + ); } var inserted = this.append_mime_type(json, toinsert); if (inserted) { diff --git a/notebook/tests/notebook/prompt_numbers.js b/notebook/tests/notebook/prompt_numbers.js index 21e6acd27..b4ca03e66 100644 --- a/notebook/tests/notebook/prompt_numbers.js +++ b/notebook/tests/notebook/prompt_numbers.js @@ -21,16 +21,16 @@ casper.notebook_test(function () { var a = 'print("a")'; var index = this.append_cell(a); - this.test.assertEquals(get_prompt(index), "In [ ]:", "prompt number is   by default"); + this.test.assertEquals(get_prompt(index), "In [ ]:", "prompt number is   by default"); set_prompt(index, 2); - this.test.assertEquals(get_prompt(index), "In [2]:", "prompt number is 2"); + this.test.assertEquals(get_prompt(index), "In [2]:", "prompt number is 2"); set_prompt(index, 0); - this.test.assertEquals(get_prompt(index), "In [0]:", "prompt number is 0"); + this.test.assertEquals(get_prompt(index), "In [0]:", "prompt number is 0"); set_prompt(index, "*"); - this.test.assertEquals(get_prompt(index), "In [*]:", "prompt number is *"); + this.test.assertEquals(get_prompt(index), "In [*]:", "prompt number is *"); set_prompt(index, undefined); - this.test.assertEquals(get_prompt(index), "In [ ]:", "prompt number is  "); + this.test.assertEquals(get_prompt(index), "In [ ]:", "prompt number is  "); set_prompt(index, null); - this.test.assertEquals(get_prompt(index), "In [ ]:", "prompt number is  "); + this.test.assertEquals(get_prompt(index), "In [ ]:", "prompt number is  "); }); }); From b72ab070b3996dc077fbeff2356b0fb237543d75 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 10 Feb 2017 16:25:34 -0500 Subject: [PATCH 002/255] Add a clearOutput code cell event This is triggered when a code cell clears its output. --- notebook/static/notebook/js/codecell.js | 1 + 1 file changed, 1 insertion(+) diff --git a/notebook/static/notebook/js/codecell.js b/notebook/static/notebook/js/codecell.js index 54781cf6f..20d217919 100644 --- a/notebook/static/notebook/js/codecell.js +++ b/notebook/static/notebook/js/codecell.js @@ -510,6 +510,7 @@ define([ CodeCell.prototype.clear_output = function (wait) { this.output_area.clear_output(wait); this.set_input_prompt(); + this.events.trigger('clearOutput.CodeCell', {cell: this}); }; From cb247590c7607ef544dfba10fb2eeacea37b6daf Mon Sep 17 00:00:00 2001 From: Tony Cebzanov Date: Mon, 13 Feb 2017 23:51:32 -0500 Subject: [PATCH 003/255] Support ANSI underline and inverse properties. * Parse underline and inverse in ANSI escape codes * Add CSS classes for same, using a subtle outline for inverse --- notebook/static/base/js/utils.js | 18 ++++++++++++++++++ notebook/static/notebook/less/ansicolors.less | 2 ++ 2 files changed, 20 insertions(+) diff --git a/notebook/static/base/js/utils.js b/notebook/static/base/js/utils.js index 93df51748..58d30299c 100644 --- a/notebook/static/base/js/utils.js +++ b/notebook/static/base/js/utils.js @@ -282,6 +282,8 @@ define([ var fg = []; var bg = []; var bold = false; + var underline = false; + var inverse = false; var match; var out = []; var numbers = []; @@ -330,6 +332,14 @@ define([ classes.push("ansi-bold"); } + if (underline) { + classes.push("ansi-underline"); + } + + if (inverse) { + classes.push("ansi-inverse"); + } + if (classes.length || styles.length) { out.push(" Date: Tue, 14 Feb 2017 00:04:01 -0500 Subject: [PATCH 004/255] Add unerline and inverse to ANSI test notebook. --- tools/tests/ANSI Test.ipynb | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tools/tests/ANSI Test.ipynb b/tools/tests/ANSI Test.ipynb index 98c91ffbb..e637b101e 100644 --- a/tools/tests/ANSI Test.ipynb +++ b/tools/tests/ANSI Test.ipynb @@ -30,6 +30,42 @@ "RESET = ESC + \"00m\"" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Bold, underline and inverse text" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This is normal text\n", + "\n", + "\u001b[01mThis is bold text\n", + "\n", + "\u001b[04mThis is underlined text\n", + "\n", + "\u001b[07mThis is inverse text\n" + ] + } + ], + "source": [ + "print (\"This is normal text\")\n", + "print()\n", + "print (\"{ESC}01mThis is bold text\".format(**locals()))\n", + "print()\n", + "print (\"{ESC}04mThis is underlined text\".format(**locals()))\n", + "print()\n", + "print (\"{ESC}07mThis is inverse text\".format(**locals()))" + ] + }, { "cell_type": "markdown", "metadata": {}, From d3cd8583a27fe132a5304db6aa5fa0ffd47689db Mon Sep 17 00:00:00 2001 From: Tony Cebzanov Date: Tue, 14 Feb 2017 08:56:55 -0500 Subject: [PATCH 005/255] Fix ANSI bold/underline/inverse test to reset after each line. --- tools/tests/ANSI Test.ipynb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/tests/ANSI Test.ipynb b/tools/tests/ANSI Test.ipynb index e637b101e..c8d193cfe 100644 --- a/tools/tests/ANSI Test.ipynb +++ b/tools/tests/ANSI Test.ipynb @@ -48,22 +48,22 @@ "text": [ "This is normal text\n", "\n", - "\u001b[01mThis is bold text\n", + "\u001b[01mThis is bold text\u001b[00m\n", "\n", - "\u001b[04mThis is underlined text\n", + "\u001b[04mThis is underlined text\u001b[00m\n", "\n", - "\u001b[07mThis is inverse text\n" + "\u001b[07mThis is inverse text\u001b[00m\n" ] } ], "source": [ "print (\"This is normal text\")\n", "print()\n", - "print (\"{ESC}01mThis is bold text\".format(**locals()))\n", + "print (\"{ESC}01mThis is bold text{RESET}\".format(**locals()))\n", "print()\n", - "print (\"{ESC}04mThis is underlined text\".format(**locals()))\n", + "print (\"{ESC}04mThis is underlined text{RESET}\".format(**locals()))\n", "print()\n", - "print (\"{ESC}07mThis is inverse text\".format(**locals()))" + "print (\"{ESC}07mThis is inverse text{RESET}\".format(**locals()))" ] }, { From 971c63d78064dd8ebe987a4bee5cd01eecccb072 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Tue, 14 Feb 2017 12:23:19 -0500 Subject: [PATCH 006/255] Update codecell.js --- notebook/static/notebook/js/codecell.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/static/notebook/js/codecell.js b/notebook/static/notebook/js/codecell.js index 20d217919..0f6913c9a 100644 --- a/notebook/static/notebook/js/codecell.js +++ b/notebook/static/notebook/js/codecell.js @@ -510,7 +510,7 @@ define([ CodeCell.prototype.clear_output = function (wait) { this.output_area.clear_output(wait); this.set_input_prompt(); - this.events.trigger('clearOutput.CodeCell', {cell: this}); + this.events.trigger('clear_output.CodeCell', {cell: this}); }; From 23d55207d9a313857d597b655e3d673a5850a1c5 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Tue, 14 Feb 2017 15:29:49 -0800 Subject: [PATCH 007/255] UX: Put notebook first on new menu See #2182 --- notebook/templates/tree.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/notebook/templates/tree.html b/notebook/templates/tree.html index 8848512b0..50e084bc1 100644 --- a/notebook/templates/tree.html +++ b/notebook/templates/tree.html @@ -54,6 +54,9 @@ data-terminals-available="{{terminals_available}}"
From 1a412fc6c2aa0ce0c4ec098267d75af23c3040f8 Mon Sep 17 00:00:00 2001 From: michaelpacer Date: Tue, 14 Feb 2017 15:15:08 -0800 Subject: [PATCH 008/255] Use form not div so that enter can submit changes --- notebook/static/notebook/js/shortcuteditor.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/notebook/static/notebook/js/shortcuteditor.js b/notebook/static/notebook/js/shortcuteditor.js index 0077dec58..9d50b7a84 100644 --- a/notebook/static/notebook/js/shortcuteditor.js +++ b/notebook/static/notebook/js/shortcuteditor.js @@ -45,14 +45,18 @@ var KeyBinding = createClass({ var that = this; var available = this.props.available(this.state.shrt); var empty = (this.state.shrt === ''); - return createElement('div', {className:'jupyter-keybindings'}, + var binding_setter = function(){ + if (available) { + that.props.onAddBindings(that.state.shrt, that.props.ckey); + } + that.state.shrt=''; + return false; + }; + return createElement('form', {className:'jupyter-keybindings', + onSubmit: binding_setter + }, createElement('i', {className: "pull-right fa fa-plus", alt: 'add-keyboard-shortcut', - onClick: function () { - if (available) { - that.props.onAddBindings(that.state.shrt, that.props.ckey); - } - that.state.shrt=''; - } + onClick: binding_setter }), createElement('input', { type:'text', From ebb1f74172e04d2f9c458f5450be8a0e8343f652 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Feb 2017 14:55:15 +0000 Subject: [PATCH 009/255] release 5.0.0b1 --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index cb9d8670a..d05b53065 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -9,5 +9,5 @@ store the current version info of the notebook. # Next beta/alpha/rc release: The version number for beta is X.Y.ZbN **without dots**. -version_info = (5, 0, 0, '.dev') +version_info = (5, 0, 0, 'b1') __version__ = '.'.join(map(str, version_info[:3])) + ''.join(version_info[3:]) diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 22f47bafb..30154ef96 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "5.0.0.dev"; + Jupyter.version = "5.0.0b1"; Jupyter._target = '_blank'; return Jupyter; }); From 747fcf0d6a5ba774625b0ad3d7c8b811f0d8c699 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 15 Feb 2017 14:55:51 +0000 Subject: [PATCH 010/255] Back to development --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index d05b53065..cb9d8670a 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -9,5 +9,5 @@ store the current version info of the notebook. # Next beta/alpha/rc release: The version number for beta is X.Y.ZbN **without dots**. -version_info = (5, 0, 0, 'b1') +version_info = (5, 0, 0, '.dev') __version__ = '.'.join(map(str, version_info[:3])) + ''.join(version_info[3:]) diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 30154ef96..22f47bafb 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "5.0.0b1"; + Jupyter.version = "5.0.0.dev"; Jupyter._target = '_blank'; return Jupyter; }); From c5711ebc093b5ae18953b7811994ac8979b5803c Mon Sep 17 00:00:00 2001 From: michaelpacer Date: Wed, 15 Feb 2017 12:31:23 -0800 Subject: [PATCH 011/255] Large structural changes to the keybinding help interface. --- notebook/static/notebook/js/shortcuteditor.js | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/notebook/static/notebook/js/shortcuteditor.js b/notebook/static/notebook/js/shortcuteditor.js index 9d50b7a84..3f343ead7 100644 --- a/notebook/static/notebook/js/shortcuteditor.js +++ b/notebook/static/notebook/js/shortcuteditor.js @@ -110,14 +110,33 @@ var KeyBindingList = createClass({ {__html: marked( - "This dialog allows you to modify the keymap of the command mode, and persist the changes. "+ - "You can define many type of shorctuts and sequences of keys. "+ - "\n\n"+ - " - Use dashes `-` to represent keys that should be pressed with modifiers, "+ - "for example `Shift-a`, or `Ctrl-;`. \n"+ - " - Separate by commas if the keys need to be pressed in sequence: `h,a,l,t`.\n"+ - "\n\nYou can combine the two: `Ctrl-x,Meta-c,Meta-b,u,t,t,e,r,f,l,y`.\n"+ - "Casing will have no effects: (e.g: `;` and `:` are the same on english keyboards)."+ + "This dialog allows you to modify the keyboard shortcuts available in command mode. "+ + "Any changes will be persisted between sessions and across environments. "+ + "You can define two kinds of shorctuts key combinations and key sequences.\n"+ + "\n"+ + " - **Key Combinations**:\n"+ + " - Use hyphens `-` to represent keys that should be pressed at the same time.\n"+ + " - This is designed for use with *modifier* keys: `Cmd`, `Ctrl`, `Alt` ,`Meta`, "+ + "`Cmdtrl`, and `Shift`.\n"+ + " - At most, one non-modifier key can exist in a key combination.\n"+ + " - Multiple non-modifier key can exist in a key combination.\n"+ + " - Modifier keys need to precede the non-modifier key in a combination.\n"+ + " - *Valid Examples*: `Shift-a`, `Ctrl-;`, or `Ctrl-Shift-a`. \n"+ + " - *Invalid Example*s: `a-b` and `a-Ctrl-Shift`. \n"+ + " - **Key Sequences**:\n"+ + " - Use commas `,` to represent keys that should be pressed in sequence.\n"+ + " - The order in which keys must be pressed exactly matches the left-to-right order of "+ + "the characters in the sequence, with no interruptions.\n"+ + " - E.g., `h,a,l,t` would be triggered by typing h a "+ + "l t but not h a a l "+ + "t or a h l t.\n"+ + " - Sequences can include the same key multiple times (e.g., `d,d`).\n"+ + " - You cannot include a sequence that is a 'prefix' of another sequence.\n"+ + " - E.g., `d,d,d` cannot be used a tthe same time as `d,d`).\n"+ + " - Key combinations are unique elements that can be used in a sequence.\n"+ + " - E.g., `Ctrl-d,d` and `d,d` can exist at the same time and are both valid key sequences.\n"+ + "\n"+ + "The case of elements will have no effects: (e.g: `;` and `:` are the same on english keyboards)."+ " You need to explicitelty write the `Shift` modifier.\n"+ "Valid modifiers are `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. Refer to developer docs "+ "for the corresponding keys depending on the platform."+ From 6b08b86cb8739abd0bebb1feb33c8bfb4d00e879 Mon Sep 17 00:00:00 2001 From: michaelpacer Date: Wed, 15 Feb 2017 17:19:22 -0800 Subject: [PATCH 012/255] Some polishing, variable name change, etc. --- notebook/static/notebook/js/shortcuteditor.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/notebook/static/notebook/js/shortcuteditor.js b/notebook/static/notebook/js/shortcuteditor.js index 3f343ead7..955135e6e 100644 --- a/notebook/static/notebook/js/shortcuteditor.js +++ b/notebook/static/notebook/js/shortcuteditor.js @@ -93,7 +93,7 @@ var KeyBindingList = createClass({ }, render: function() { var that = this; - var childrens = this.state.data.map(function (binding) { + var children = this.state.data.map(function (binding) { return createElement(KeyBinding, Object.assign({}, binding, { onAddBindings: function (shortcut, action) { that.props.bind(shortcut, action); @@ -106,13 +106,13 @@ var KeyBindingList = createClass({ } })); }); - childrens.unshift(createElement('div', {className:'well', key:'disclamer', dangerouslySetInnerHTML: + children.unshift(createElement('div', {className:'well', key:'disclamer', dangerouslySetInnerHTML: {__html: marked( "This dialog allows you to modify the keyboard shortcuts available in command mode. "+ "Any changes will be persisted between sessions and across environments. "+ - "You can define two kinds of shorctuts key combinations and key sequences.\n"+ + "You can define two kinds of shorctuts **key combinations** and **key sequences**.\n"+ "\n"+ " - **Key Combinations**:\n"+ " - Use hyphens `-` to represent keys that should be pressed at the same time.\n"+ @@ -136,16 +136,22 @@ var KeyBindingList = createClass({ " - Key combinations are unique elements that can be used in a sequence.\n"+ " - E.g., `Ctrl-d,d` and `d,d` can exist at the same time and are both valid key sequences.\n"+ "\n"+ - "The case of elements will have no effects: (e.g: `;` and `:` are the same on english keyboards)."+ - " You need to explicitelty write the `Shift` modifier.\n"+ - "Valid modifiers are `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. Refer to developer docs "+ - "for the corresponding keys depending on the platform."+ + "**Additional notes:**\n"+ + "The case in which elements are written does not change the binding's meaning. "+ + "E.g., `Ctrl-D` and `cTrl-d` are the same key binding. "+ + "Thus, `Shift` needs to be explicitly included if it is part of the key binding. "+ + "So, for example, if you set a command to be activated by `Shift-D,D`, the second `d` "+ + "cannot be pressed at the same time as the `Shift` modifier key.\n"+ + "Valid modifiers are specified by writing out their names explicitly: "+ + "e.g., `Shift`, `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. You cannot use the symbol equivalents "+ + "(e.g., `⇧`, `⌘`, `⌃`, `⌥`); refer to developer docs for the corresponding keys "+ + "(the mapping of which depends on the platform you are using)."+ "You can hover on the name/description of a command to see its exact internal name and "+ - "differentiate from actions defined in various plugins. Changing the "+ - "keybindings of edit mode is not yet possible." + "differentiate from actions defined in various plugins. \n"+ + "Changing the keybindings of edit mode is not currently available." )} })); - return createElement('div',{}, childrens); + return createElement('div',{}, children); } }); From b91c9218c0614a2f239857fcad7f5d02891586cd Mon Sep 17 00:00:00 2001 From: michaelpacer Date: Wed, 15 Feb 2017 17:28:52 -0800 Subject: [PATCH 013/255] Add line breaks --- notebook/static/notebook/js/shortcuteditor.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/notebook/static/notebook/js/shortcuteditor.js b/notebook/static/notebook/js/shortcuteditor.js index 955135e6e..f92efff4b 100644 --- a/notebook/static/notebook/js/shortcuteditor.js +++ b/notebook/static/notebook/js/shortcuteditor.js @@ -112,12 +112,12 @@ var KeyBindingList = createClass({ "This dialog allows you to modify the keyboard shortcuts available in command mode. "+ "Any changes will be persisted between sessions and across environments. "+ - "You can define two kinds of shorctuts **key combinations** and **key sequences**.\n"+ + "You can define two kinds of shorctuts: **key combinations** and **key sequences**.\n"+ "\n"+ " - **Key Combinations**:\n"+ " - Use hyphens `-` to represent keys that should be pressed at the same time.\n"+ " - This is designed for use with *modifier* keys: `Cmd`, `Ctrl`, `Alt` ,`Meta`, "+ - "`Cmdtrl`, and `Shift`.\n"+ + "`Cmdtrl`, and `Shift`.\n"+ " - At most, one non-modifier key can exist in a key combination.\n"+ " - Multiple non-modifier key can exist in a key combination.\n"+ " - Modifier keys need to precede the non-modifier key in a combination.\n"+ @@ -132,22 +132,25 @@ var KeyBindingList = createClass({ "t or a h l t.\n"+ " - Sequences can include the same key multiple times (e.g., `d,d`).\n"+ " - You cannot include a sequence that is a 'prefix' of another sequence.\n"+ - " - E.g., `d,d,d` cannot be used a tthe same time as `d,d`).\n"+ + " - E.g., `d,d,d` cannot be used a the same time as `d,d`).\n"+ " - Key combinations are unique elements that can be used in a sequence.\n"+ " - E.g., `Ctrl-d,d` and `d,d` can exist at the same time and are both valid key sequences.\n"+ "\n"+ - "**Additional notes:**\n"+ + "**Additional notes**:\n"+ + "\n"+ "The case in which elements are written does not change the binding's meaning. "+ "E.g., `Ctrl-D` and `cTrl-d` are the same key binding. "+ "Thus, `Shift` needs to be explicitly included if it is part of the key binding. "+ "So, for example, if you set a command to be activated by `Shift-D,D`, the second `d` "+ "cannot be pressed at the same time as the `Shift` modifier key.\n"+ + "\n"+ "Valid modifiers are specified by writing out their names explicitly: "+ "e.g., `Shift`, `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. You cannot use the symbol equivalents "+ "(e.g., `⇧`, `⌘`, `⌃`, `⌥`); refer to developer docs for the corresponding keys "+ "(the mapping of which depends on the platform you are using)."+ "You can hover on the name/description of a command to see its exact internal name and "+ "differentiate from actions defined in various plugins. \n"+ + "\n"+ "Changing the keybindings of edit mode is not currently available." )} })); From 8543b4e9f636471ff38f1f11a67fbb37148d1925 Mon Sep 17 00:00:00 2001 From: michaelpacer Date: Fri, 17 Feb 2017 14:59:02 -0800 Subject: [PATCH 014/255] Put short description at top, longer at bottom --- notebook/static/notebook/js/shortcuteditor.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/notebook/static/notebook/js/shortcuteditor.js b/notebook/static/notebook/js/shortcuteditor.js index f92efff4b..1ddc26294 100644 --- a/notebook/static/notebook/js/shortcuteditor.js +++ b/notebook/static/notebook/js/shortcuteditor.js @@ -106,10 +106,19 @@ var KeyBindingList = createClass({ } })); }); - children.unshift(createElement('div', {className:'well', key:'disclamer', dangerouslySetInnerHTML: + children.unshift(createElement('div', {className:'well', key:'disclamer', id:'short-key-binding-intro', dangerouslySetInnerHTML: {__html: marked( - + + "Here you can modify the keyboard shortcuts available in "+ + "command mode. Your changes will be stored for later sessions. "+ + "See more [**details of defining keyboard shortcuts**](#long-key-binding-intro) below." + )} + })); + children.push(createElement('div', {className:'well', key:'disclamer', id:'long-key-binding-intro', dangerouslySetInnerHTML: + {__html: + marked( + "This dialog allows you to modify the keyboard shortcuts available in command mode. "+ "Any changes will be persisted between sessions and across environments. "+ "You can define two kinds of shorctuts: **key combinations** and **key sequences**.\n"+ From 240186ab4875cced7d70d16eb3d7eb03461506d7 Mon Sep 17 00:00:00 2001 From: michaelpacer Date: Fri, 17 Feb 2017 15:03:07 -0800 Subject: [PATCH 015/255] Add Cmdtrl description --- notebook/static/notebook/js/shortcuteditor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/notebook/static/notebook/js/shortcuteditor.js b/notebook/static/notebook/js/shortcuteditor.js index 1ddc26294..d122d688d 100644 --- a/notebook/static/notebook/js/shortcuteditor.js +++ b/notebook/static/notebook/js/shortcuteditor.js @@ -127,6 +127,7 @@ var KeyBindingList = createClass({ " - Use hyphens `-` to represent keys that should be pressed at the same time.\n"+ " - This is designed for use with *modifier* keys: `Cmd`, `Ctrl`, `Alt` ,`Meta`, "+ "`Cmdtrl`, and `Shift`.\n"+ + " - `Cmdtrl` acts like `Cmd` on OS X/MacOS and `Ctrl` on Windows/Linux.\n"+ " - At most, one non-modifier key can exist in a key combination.\n"+ " - Multiple non-modifier key can exist in a key combination.\n"+ " - Modifier keys need to precede the non-modifier key in a combination.\n"+ From 1f93cb6b544e9b1d4c0e07888e8652d81ff7ea47 Mon Sep 17 00:00:00 2001 From: michaelpacer Date: Fri, 17 Feb 2017 15:04:23 -0800 Subject: [PATCH 016/255] Clarify prefix exclusion --- notebook/static/notebook/js/shortcuteditor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook/static/notebook/js/shortcuteditor.js b/notebook/static/notebook/js/shortcuteditor.js index d122d688d..be0c2c434 100644 --- a/notebook/static/notebook/js/shortcuteditor.js +++ b/notebook/static/notebook/js/shortcuteditor.js @@ -141,8 +141,8 @@ var KeyBindingList = createClass({ "l t but not h a a l "+ "t or a h l t.\n"+ " - Sequences can include the same key multiple times (e.g., `d,d`).\n"+ - " - You cannot include a sequence that is a 'prefix' of another sequence.\n"+ - " - E.g., `d,d,d` cannot be used a the same time as `d,d`).\n"+ + " - You cannot include any pairs of sequences where one is a 'prefix' the other.\n"+ + " - E.g., `d,d,d` cannot be used a the same time as `d,d`.\n"+ " - Key combinations are unique elements that can be used in a sequence.\n"+ " - E.g., `Ctrl-d,d` and `d,d` can exist at the same time and are both valid key sequences.\n"+ "\n"+ From f384662df4465b4dba881304059843c3f5072390 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 20 Feb 2017 13:28:06 +0000 Subject: [PATCH 017/255] Fix viewing HTML in sandboxed iframe See gh-2203 The URL calculation was going wrong, so it was using a URL starting with //files. This uses url_path_join() to get the separators right. --- notebook/templates/view.html | 2 +- notebook/view/handlers.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/notebook/templates/view.html b/notebook/templates/view.html index 3e2dd54f2..935b397ce 100644 --- a/notebook/templates/view.html +++ b/notebook/templates/view.html @@ -23,7 +23,7 @@ }
- +
diff --git a/notebook/view/handlers.py b/notebook/view/handlers.py index 4d7e97844..4496a5294 100644 --- a/notebook/view/handlers.py +++ b/notebook/view/handlers.py @@ -6,7 +6,7 @@ from tornado import web from ..base.handlers import IPythonHandler, path_regex -from ..utils import url_escape +from ..utils import url_escape, url_path_join class ViewHandler(IPythonHandler): """Render HTML files within an iframe.""" @@ -17,8 +17,9 @@ class ViewHandler(IPythonHandler): raise web.HTTPError(404, u'File does not exist: %s' % path) basename = path.rsplit('/', 1)[-1] + file_url = url_path_join(self.base_url, 'files', path) self.write( - self.render_template('view.html', file_path=url_escape(path), page_title=basename) + self.render_template('view.html', file_url=file_url, page_title=basename) ) default_handlers = [ From 1e3eb0e0e78f597230a8b17022a8825738453ead Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 20 Feb 2017 15:30:30 +0100 Subject: [PATCH 018/255] [Tags UI] Replace X with FA icon --- .../static/notebook/js/celltoolbarpresets/tags.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/notebook/static/notebook/js/celltoolbarpresets/tags.js b/notebook/static/notebook/js/celltoolbarpresets/tags.js index 6289e7082..7508b0ab2 100644 --- a/notebook/static/notebook/js/celltoolbarpresets/tags.js +++ b/notebook/static/notebook/js/celltoolbarpresets/tags.js @@ -13,7 +13,7 @@ define([ return a.filter(function(n) { return b.indexOf(n) === -1; }); - } + }; var write_tag = function(cell, name, add) { if (add) { @@ -43,12 +43,12 @@ define([ } cell.events.trigger('set_dirty.Notebook', {value: true}); return true; - } + }; var preprocess_input = function(input) { // Split on whitespace: return input.split(/\s/); - } + }; var add_tag = function(cell, tag_container, on_remove) { return function(name) { @@ -111,9 +111,9 @@ define([ .addClass('cell-tag') .text(name); - var remove_button = $('') + var remove_button = $('') .addClass('remove-tag-btn') - .text('X') + .addClass('fa fa-times') .click( function(ev) { if (ev.button === 0) { on_remove(name); @@ -148,7 +148,7 @@ define([ } }); var input_container = $('') - .addClass('tags-input') + .addClass('tags-input'); add_dialog_button(input_container, cell, on_add, on_remove); button_container.append(input_container .append(text) From 1f11cd1abffa68cc26786f226f53e590f19e66a0 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 20 Feb 2017 15:31:41 +0100 Subject: [PATCH 019/255] [Tags UI] Style input controls according to review Fixes most of style review in #2205. --- notebook/static/notebook/less/tagbar.less | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/notebook/static/notebook/less/tagbar.less b/notebook/static/notebook/less/tagbar.less index 459776a08..151d3c9f3 100644 --- a/notebook/static/notebook/less/tagbar.less +++ b/notebook/static/notebook/less/tagbar.less @@ -35,6 +35,29 @@ background: linear-gradient(to right, rgba(0,0,0,0), #EEE); } -.tags-dialog-btn { - margin-right: 4px; +.tags-input > * { + margin-left: 4px; +} + +.cell-tag, +.tags-input input, +.tags-input button { + .form-control(); + .input-sm(); + // undo some of the sizing caused by the above mixins + width: inherit; + font-size: inherit; + height: 22px; + padding: 0px; + + display: inline-block; +} + +.cell-tag, +.tags-input button { + padding: 0px 4px; +} + +.cell-tag { + background-color: #fff; } From 2d42e62e85317374435a172476fed03ca36efd59 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 21 Feb 2017 12:08:54 +0100 Subject: [PATCH 020/255] Don't use jquery-ui data selector since jquery-ui isn't available iterate over list items instead (result is the same) --- notebook/static/tree/js/notebooklist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index f9724c0a7..94525e6d6 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -1024,7 +1024,7 @@ define([ * Remove the deleted notebook. */ var that = this; - $( ":data(path)" ).each(function() { + $(".list_item").each(function() { var element = $(this); if (element.data("path") === path) { element.remove(); From 4000838321c90e0a03905dac6989598912548c7a Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Wed, 22 Feb 2017 11:13:34 -0800 Subject: [PATCH 021/255] Cull idle kernels after specified period --- notebook/services/kernels/kernelmanager.py | 70 +++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index b5c4c9f5d..a3b8d5121 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -11,15 +11,17 @@ import os from tornado import gen, web from tornado.concurrent import Future -from tornado.ioloop import IOLoop +from tornado.ioloop import IOLoop, PeriodicCallback from jupyter_client.multikernelmanager import MultiKernelManager -from traitlets import Dict, List, Unicode, TraitError, default, validate +from traitlets import Dict, List, Unicode, TraitError, Integer, default, validate from notebook.utils import to_os_path from notebook._tz import utcnow, isoformat from ipython_genutils.py3compat import getcwd +from datetime import datetime, timedelta + class MappingKernelManager(MultiKernelManager): """A KernelManager that handles notebook mapping and HTTP error handling""" @@ -34,6 +36,10 @@ class MappingKernelManager(MultiKernelManager): _kernel_connections = Dict() + _culler_callback = None + + _initialized_culler = False + @default('root_dir') def _default_root_dir(self): try: @@ -52,6 +58,26 @@ class MappingKernelManager(MultiKernelManager): raise TraitError("kernel root dir %r is not a directory" % value) return value + cull_kernels_after_minutes_env = 'CULL_KERNELS_AFTER_MINUTES' + cull_kernels_after_minutes_default = 0 + cull_kernels_after_minutes = Integer(cull_kernels_after_minutes_default, config=True, + help="""Duration (minutes) in which a kernel must remain idle before it can be culled. Culling is disabled (0) by default.""" + ) + + @default('cull_kernels_after_minutes') + def cull_kernels_after_minutes_value(self): + return int(os.getenv(self.cull_kernels_after_minutes_env, self.cull_kernels_after_minutes_default)) + + kernel_culling_interval_seconds_env = 'KERNEL_CULLING_INTERVAL_SECONDS' + kernel_culling_interval_seconds_default = 300 # 5 minutes + kernel_culling_interval_seconds = Integer(kernel_culling_interval_seconds_default, config=True, + help="""The interval (seconds) in which kernels are culled if exceeding the idle duration.""" + ) + + @default('kernel_culling_interval_seconds') + def kernel_culling_interval_seconds_value(self): + return int(os.getenv(self.kernel_culling_interval_seconds_env, self.kernel_culling_interval_seconds_default)) + #------------------------------------------------------------------------- # Methods for managing kernels and sessions #------------------------------------------------------------------------- @@ -105,6 +131,11 @@ class MappingKernelManager(MultiKernelManager): else: self._check_kernel_id(kernel_id) self.log.info("Using existing kernel: %s" % kernel_id) + + # Initialize culling if not already + if not self._initialized_culler: + self.initialize_culler() + # py2-compat raise gen.Return(kernel_id) @@ -225,3 +256,38 @@ class MappingKernelManager(MultiKernelManager): kernel._activity_stream.on_recv(record_activity) + def initialize_culler(self): + """Start idle culler if 'cull_kernels_after_minutes' is greater than zero. + + Regardless of that value, set flag that we've been here. + """ + if not self._initialized_culler and self.cull_kernels_after_minutes > 0: + if self._culler_callback is None: + loop = IOLoop.current() + self._culler_callback = PeriodicCallback( + self.cull_kernels, 1000*self.kernel_culling_interval_seconds, loop) + self.log.info("Culling kernels with idle durations > %s minutes at %s second intervals ...", + self.cull_kernels_after_minutes, self.kernel_culling_interval_seconds) + self._culler_callback.start() + + self._initialized_culler = True + + def cull_kernels(self): + self.log.debug("Polling every %s seconds for kernels idle > %s minutes...", + self.kernel_culling_interval_seconds, self.cull_kernels_after_minutes) + for kId, kernel in self._kernels.items(): + self.cull_kernel(kId, kernel) + + def cull_kernel(self, kId, kernel): + activity = kernel.last_activity + name = kernel.kernel_name + self.log.debug("kId=%s, name=%s, last_activity=%s", kId, name, activity) + if activity is not None: + dtNow = utcnow() + #dtActivity = datetime.strptime(activity,'%Y-%m-%dT%H:%M:%S.%f') + dtIdle = dtNow - activity + if dtIdle > timedelta(minutes=self.cull_kernels_after_minutes): # can be culled + idleDuration = int(dtIdle.total_seconds()/60.0) + self.log.warn("Culling kernel '%s' (%s) due to %s minutes of inactivity.", name, kId, idleDuration) + self.shutdown_kernel(kId) + From 2f787088d5d3d009edc7086a90e8b7283763e04f Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Wed, 22 Feb 2017 11:59:34 -0800 Subject: [PATCH 022/255] Validate culling interval. --- notebook/services/kernels/kernelmanager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index a3b8d5121..71f265683 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -264,6 +264,10 @@ class MappingKernelManager(MultiKernelManager): if not self._initialized_culler and self.cull_kernels_after_minutes > 0: if self._culler_callback is None: loop = IOLoop.current() + if self.kernel_culling_interval_seconds <= 0: #handle case where user set invalid value + self.log.warn("Invalid value for 'kernel_culling_interval_seconds' detected (%s) - using default value (%s).", + self.kernel_culling_interval_seconds, self.kernel_culling_interval_seconds_default) + self.kernel_culling_interval_seconds = self.kernel_culling_interval_seconds_default self._culler_callback = PeriodicCallback( self.cull_kernels, 1000*self.kernel_culling_interval_seconds, loop) self.log.info("Culling kernels with idle durations > %s minutes at %s second intervals ...", From 7733aeb8aed17f2740648aa55d9c815d62315dd5 Mon Sep 17 00:00:00 2001 From: Lorenzo Gasparini Date: Thu, 23 Feb 2017 10:34:11 +0100 Subject: [PATCH 023/255] Fixed issue 980 --- notebook/static/notebook/js/mathjaxutils.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/notebook/static/notebook/js/mathjaxutils.js b/notebook/static/notebook/js/mathjaxutils.js index 4f6b1d5aa..fa24a93d2 100644 --- a/notebook/static/notebook/js/mathjaxutils.js +++ b/notebook/static/notebook/js/mathjaxutils.js @@ -59,7 +59,7 @@ define([ // MATHSPLIT contains the pattern for math delimiters and special symbols // needed for searching for math in the text input. - var MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@)/i; + var MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)))/i; // The math is in blocks i through j, so // collect it into one block and clear the others. @@ -178,6 +178,11 @@ define([ end = block; braces = 0; } + else if (block === "\\\\\(") { + start = i; + end = "\\\\\)"; + braces = 0; + } else if (block.substr(1, 5) === "begin") { start = i; end = "\\end" + block.substr(6); @@ -200,7 +205,9 @@ define([ // var replace_math = function (text, math) { text = text.replace(/@@(\d+)@@/g, function (match, n) { - return math[n]; + return math[n] + .replace("\\\\\(", "\\\(") + .replace("\\\\\)", "\\\)"); }); return text; }; From 2b4145e50621cceb46a071690555bc7bb60469e1 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Thu, 23 Feb 2017 15:04:39 +0100 Subject: [PATCH 024/255] [TagsUI] Prevent highlighting of tagbar input box --- notebook/static/notebook/less/tagbar.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/notebook/static/notebook/less/tagbar.less b/notebook/static/notebook/less/tagbar.less index 151d3c9f3..0cec30a50 100644 --- a/notebook/static/notebook/less/tagbar.less +++ b/notebook/static/notebook/less/tagbar.less @@ -61,3 +61,9 @@ .cell-tag { background-color: #fff; } + +.tags-input input[type=text]:focus { + outline: none; + box-shadow: none; + border-color: #ccc; +} From c3f753faf87cb33ce5de7327d47998424965fb21 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Thu, 23 Feb 2017 08:36:00 -0800 Subject: [PATCH 025/255] incorporate review recommendations --- notebook/services/kernels/kernelmanager.py | 71 +++++++++------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index 71f265683..9bf6d8ab9 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -58,26 +58,15 @@ class MappingKernelManager(MultiKernelManager): raise TraitError("kernel root dir %r is not a directory" % value) return value - cull_kernels_after_minutes_env = 'CULL_KERNELS_AFTER_MINUTES' - cull_kernels_after_minutes_default = 0 - cull_kernels_after_minutes = Integer(cull_kernels_after_minutes_default, config=True, - help="""Duration (minutes) in which a kernel must remain idle before it can be culled. Culling is disabled (0) by default.""" + cull_idle_timeout = Integer(0, config=True, + help="""Timeout (in seconds) after which a kernel is considered idle and ready to be culled.""" ) - @default('cull_kernels_after_minutes') - def cull_kernels_after_minutes_value(self): - return int(os.getenv(self.cull_kernels_after_minutes_env, self.cull_kernels_after_minutes_default)) - - kernel_culling_interval_seconds_env = 'KERNEL_CULLING_INTERVAL_SECONDS' - kernel_culling_interval_seconds_default = 300 # 5 minutes - kernel_culling_interval_seconds = Integer(kernel_culling_interval_seconds_default, config=True, - help="""The interval (seconds) in which kernels are culled if exceeding the idle duration.""" + cull_interval_default = 300 # 5 minutes + cull_interval = Integer(cull_interval_default, config=True, + help="""The interval (in seconds) on which to check for idle kernels exceeding the cull timeout value.""" ) - @default('kernel_culling_interval_seconds') - def kernel_culling_interval_seconds_value(self): - return int(os.getenv(self.kernel_culling_interval_seconds_env, self.kernel_culling_interval_seconds_default)) - #------------------------------------------------------------------------- # Methods for managing kernels and sessions #------------------------------------------------------------------------- @@ -257,41 +246,39 @@ class MappingKernelManager(MultiKernelManager): kernel._activity_stream.on_recv(record_activity) def initialize_culler(self): - """Start idle culler if 'cull_kernels_after_minutes' is greater than zero. + """Start idle culler if 'cull_idle_timeout' is greater than zero. Regardless of that value, set flag that we've been here. """ - if not self._initialized_culler and self.cull_kernels_after_minutes > 0: + if not self._initialized_culler and self.cull_idle_timeout > 0: if self._culler_callback is None: loop = IOLoop.current() - if self.kernel_culling_interval_seconds <= 0: #handle case where user set invalid value - self.log.warn("Invalid value for 'kernel_culling_interval_seconds' detected (%s) - using default value (%s).", - self.kernel_culling_interval_seconds, self.kernel_culling_interval_seconds_default) - self.kernel_culling_interval_seconds = self.kernel_culling_interval_seconds_default + if self.cull_interval <= 0: #handle case where user set invalid value + self.log.warning("Invalid value for 'cull_interval' detected (%s) - using default value (%s).", + self.cull_interval, self.cull_interval_default) + self.cull_interval = self.cull_interval_default self._culler_callback = PeriodicCallback( - self.cull_kernels, 1000*self.kernel_culling_interval_seconds, loop) - self.log.info("Culling kernels with idle durations > %s minutes at %s second intervals ...", - self.cull_kernels_after_minutes, self.kernel_culling_interval_seconds) + self.cull_kernels, 1000*self.cull_interval, loop) + self.log.info("Culling kernels with idle durations > %s seconds at %s second intervals ...", + self.cull_idle_timeout, self.cull_interval) self._culler_callback.start() self._initialized_culler = True def cull_kernels(self): - self.log.debug("Polling every %s seconds for kernels idle > %s minutes...", - self.kernel_culling_interval_seconds, self.cull_kernels_after_minutes) - for kId, kernel in self._kernels.items(): - self.cull_kernel(kId, kernel) - - def cull_kernel(self, kId, kernel): - activity = kernel.last_activity - name = kernel.kernel_name - self.log.debug("kId=%s, name=%s, last_activity=%s", kId, name, activity) - if activity is not None: - dtNow = utcnow() - #dtActivity = datetime.strptime(activity,'%Y-%m-%dT%H:%M:%S.%f') - dtIdle = dtNow - activity - if dtIdle > timedelta(minutes=self.cull_kernels_after_minutes): # can be culled - idleDuration = int(dtIdle.total_seconds()/60.0) - self.log.warn("Culling kernel '%s' (%s) due to %s minutes of inactivity.", name, kId, idleDuration) - self.shutdown_kernel(kId) + self.log.debug("Polling every %s seconds for kernels idle > %s seconds...", + self.cull_interval, self.cull_idle_timeout) + for kernel_id in list(self._kernels): + self.cull_kernel_if_idle(kernel_id) + + def cull_kernel_if_idle(self, kernel_id): + kernel = self._kernels[kernel_id] + self.log.debug("kernel_id=%s, kernel_name=%s, last_activity=%s", kernel_id, kernel.kernel_name, kernel.last_activity) + if kernel.last_activity is not None: + dt_now = utcnow() + dt_idle = dt_now - kernel.last_activity + if dt_idle > timedelta(seconds=self.cull_idle_timeout): # exceeds timeout, can be culled + idle_duration = int(dt_idle.total_seconds()) + self.log.warning("Culling kernel '%s' (%s) due to %s seconds of inactivity.", kernel.kernel_name, kernel_id, idle_duration) + self.shutdown_kernel(kernel_id) From ff19c4cd7c862912822bbfbe1c1b23c0a9a3ee21 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Fri, 24 Feb 2017 10:03:30 -0800 Subject: [PATCH 026/255] enforce minimum timeout, ensure exceptions don't prevent culling of others --- notebook/services/kernels/kernelmanager.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index 9bf6d8ab9..292cb6239 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -58,8 +58,11 @@ class MappingKernelManager(MultiKernelManager): raise TraitError("kernel root dir %r is not a directory" % value) return value + cull_idle_timeout_minimum = 300 # 5 minutes cull_idle_timeout = Integer(0, config=True, - help="""Timeout (in seconds) after which a kernel is considered idle and ready to be culled.""" + help="""Timeout (in seconds) after which a kernel is considered idle and ready to be culled. Values of 0 or + lower disable culling. The minimum timeout is 300 seconds (5 minutes). Positive values less than the minimum value + will be set to the minimum.""" ) cull_interval_default = 300 # 5 minutes @@ -252,6 +255,10 @@ class MappingKernelManager(MultiKernelManager): """ if not self._initialized_culler and self.cull_idle_timeout > 0: if self._culler_callback is None: + if self.cull_idle_timeout < self.cull_idle_timeout_minimum: + self.log.warning("'cull_idle_timeout' (%s) is less than the minimum value (%s) and has been set to the minimum.", + self.cull_idle_timeout, self.cull_idle_timeout_minimum) + self.cull_idle_timeout = self.cull_idle_timeout_minimum loop = IOLoop.current() if self.cull_interval <= 0: #handle case where user set invalid value self.log.warning("Invalid value for 'cull_interval' detected (%s) - using default value (%s).", @@ -268,8 +275,13 @@ class MappingKernelManager(MultiKernelManager): def cull_kernels(self): self.log.debug("Polling every %s seconds for kernels idle > %s seconds...", self.cull_interval, self.cull_idle_timeout) + """Create a separate list of kernels to avoid conflicting updates while iterating""" for kernel_id in list(self._kernels): - self.cull_kernel_if_idle(kernel_id) + try: + self.cull_kernel_if_idle(kernel_id) + except Exception as e: + self.log.exception("The following exception was encountered while checking the idle duration of kernel %s: %s", + kernel_id, e) def cull_kernel_if_idle(self, kernel_id): kernel = self._kernels[kernel_id] From e3b89c391038141f9217e00b117df19bd753e673 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 24 Feb 2017 18:25:33 +0000 Subject: [PATCH 027/255] Update docs on enabling notebook server extensions --- docs/source/extending/handlers.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/extending/handlers.rst b/docs/source/extending/handlers.rst index f8a7dc613..aea3767e6 100644 --- a/docs/source/extending/handlers.rst +++ b/docs/source/extending/handlers.rst @@ -31,33 +31,33 @@ when the extension is loaded. To get the notebook server to load your custom extension, you'll need to add it to the list of extensions to be loaded. You can do this using the -config system. ``NotebookApp.server_extensions`` is a config variable -which is an array of strings, each a Python module to be imported. +config system. ``NotebookApp.nbserver_extensions`` is a config variable +which is an dictionary of strings, each a Python module to be imported, mapping +to ``True`` to enable or ``False`` to disable each extension. Because this variable is notebook config, you can set it two different ways, using config files or via the command line. For example, to get your extension to load via the command line add a -double dash before the variable name, and put the Python array in +double dash before the variable name, and put the Python dictionary in double quotes. If your package is "mypackage" and module is "mymodule", this would look like -``jupyter notebook --NotebookApp.server_extensions="['mypackage.mymodule']"`` +``jupyter notebook --NotebookApp.nbserver_extensions="{'mypackage.mymodule':True}"`` . Basically the string should be Python importable. Alternatively, you can have your extension loaded regardless of the command line args by setting the variable in the Jupyter config file. The default location of the Jupyter config file is -``~/.jupyter/profile_default/jupyter_notebook_config.py``. Then, inside +``~/.jupyter/jupyter_notebook_config.py`` (see :doc:`/config_overview`). Inside the config file, you can use Python to set the variable. For example, -the following config does the same as the previous command line example -[1]. +the following config does the same as the previous command line example. .. code:: python c = get_config() - c.NotebookApp.server_extensions = [ - 'mypackage.mymodule' - ] + c.NotebookApp.nbserver_extensions = { + 'mypackage.mymodule': True, + } Before continuing, it's a good idea to verify that your extension is being loaded. Use a print statement to print something unique. Launch From a38eadbfefa9da254e498842f563a130f68caed4 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 24 Feb 2017 18:26:13 +0000 Subject: [PATCH 028/255] Fix spelling --- docs/source/extending/handlers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/extending/handlers.rst b/docs/source/extending/handlers.rst index aea3767e6..90bed96a9 100644 --- a/docs/source/extending/handlers.rst +++ b/docs/source/extending/handlers.rst @@ -32,7 +32,7 @@ when the extension is loaded. To get the notebook server to load your custom extension, you'll need to add it to the list of extensions to be loaded. You can do this using the config system. ``NotebookApp.nbserver_extensions`` is a config variable -which is an dictionary of strings, each a Python module to be imported, mapping +which is a dictionary of strings, each a Python module to be imported, mapping to ``True`` to enable or ``False`` to disable each extension. Because this variable is notebook config, you can set it two different ways, using config files or via the command line. From 0be70718a534e577d612b235ee2eba4eee511844 Mon Sep 17 00:00:00 2001 From: nickylimjj Date: Sat, 25 Feb 2017 01:09:00 -0600 Subject: [PATCH 029/255] updated changelog for nbextensions --- docs/source/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 67826068a..aa06569f7 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -110,6 +110,8 @@ Other additions - The tab icon in the browser now changes to indicate when the kernel is busy (:ghpull:`1837`). +- Load server extensions with ConfigManager so that merge happens recursively, unlike normal config values to make it load more consistenly with frontend extensions(:ghpull:`2108`). + Remember that upgrading ``notebook`` only affects the user interface. Upgrading kernels and libraries may also provide new features, better stability and integration with the notebook interface. From 0d595f323c0fadbb7733dc140a36300d84b55ab2 Mon Sep 17 00:00:00 2001 From: nickylimjj Date: Sat, 25 Feb 2017 10:46:12 -0600 Subject: [PATCH 030/255] keep within 80 cols, fix typos --- docs/source/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index aa06569f7..64f8ac054 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -110,7 +110,9 @@ Other additions - The tab icon in the browser now changes to indicate when the kernel is busy (:ghpull:`1837`). -- Load server extensions with ConfigManager so that merge happens recursively, unlike normal config values to make it load more consistenly with frontend extensions(:ghpull:`2108`). +- Load server extensions with ConfigManager so that merge happens recursively, + unlike normal config values, to make it load more consistently with frontend + extensions(:ghpull:`2108`). Remember that upgrading ``notebook`` only affects the user interface. Upgrading kernels and libraries may also provide new features, From 2f266d3183f128a4d6e79cfd2d124c70a1b4c4b5 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sat, 25 Feb 2017 18:33:05 -0800 Subject: [PATCH 031/255] Only show View button for HTML files. --- notebook/static/tree/js/notebooklist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index 94525e6d6..b7d3921e9 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -601,7 +601,7 @@ define([ // View is visible when an item is renderable or downloadable if (selected.length > 0 && !has_directory && selected.every(function(el) { - return el.path.match(/html?|json|jpe?g|png|gif|tiff?|svg|bmp|ico|pdf|doc|xls/); + return el.path.match(/html?/); })) { $('.view-button').css('display', 'inline-block'); } else { From 5974cbe2ad812ff49e7889af2ba0f729aa2b1edc Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sat, 25 Feb 2017 18:40:37 -0800 Subject: [PATCH 032/255] Don't show Edit button on files we know we can't edit or ipynb This removes the showing of the Edit button when: * We know we can't edit the file (pdf, doc, xls, jpeg, png, etc.) * Or a notebook - looking at this again though... --- notebook/static/tree/js/notebooklist.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index b7d3921e9..a4d9cf23f 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -609,7 +609,9 @@ define([ } // Edit is visible when an item is editable - if (selected.length > 0 && !has_directory) { + if (selected.length > 0 && !has_directory && !selected.find(function(el) { + return el.path.match(/ipynb||jpe?g|png|gif|tiff?|bmp|ico|pdf|doc|xls/); + })) { $('.edit-button').css('display', 'inline-block'); } else { $('.edit-button').css('display', 'none'); From 6846deec7a0bc3adf7a0cac1cf906f69358b2dc1 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sat, 25 Feb 2017 18:44:02 -0800 Subject: [PATCH 033/255] Fixing notebook behavior of Edit. In the future I would like to show the Edit button for notebooks but have it open them in the text editor. Right now, there isn't any way for a user to do this. Thus hiding Edit on notebooks for now. --- notebook/static/tree/js/notebooklist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index a4d9cf23f..29192d3c7 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -610,7 +610,7 @@ define([ // Edit is visible when an item is editable if (selected.length > 0 && !has_directory && !selected.find(function(el) { - return el.path.match(/ipynb||jpe?g|png|gif|tiff?|bmp|ico|pdf|doc|xls/); + return el.path.match(/ipynb|jpe?g|png|gif|tiff?|bmp|ico|pdf|doc|xls/); })) { $('.edit-button').css('display', 'inline-block'); } else { From 9415f897762595ae7aaa99cb555c216329e6122d Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sat, 25 Feb 2017 18:55:42 -0800 Subject: [PATCH 034/255] Super minor style fixes for the cell tag UI. --- notebook/static/notebook/less/tagbar.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/notebook/static/notebook/less/tagbar.less b/notebook/static/notebook/less/tagbar.less index 0cec30a50..b32025b27 100644 --- a/notebook/static/notebook/less/tagbar.less +++ b/notebook/static/notebook/less/tagbar.less @@ -48,7 +48,8 @@ width: inherit; font-size: inherit; height: 22px; - padding: 0px; + line-height: 22px; + padding: 0px 4px; display: inline-block; } From 2e0d44b5d4d7b84c1ff1312b845aec0e3c6adf79 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 25 Feb 2017 20:18:18 -0800 Subject: [PATCH 035/255] retab with spaces. --- notebook/static/notebook/less/tagbar.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/static/notebook/less/tagbar.less b/notebook/static/notebook/less/tagbar.less index b32025b27..3d72c5c3c 100644 --- a/notebook/static/notebook/less/tagbar.less +++ b/notebook/static/notebook/less/tagbar.less @@ -48,7 +48,7 @@ width: inherit; font-size: inherit; height: 22px; - line-height: 22px; + line-height: 22px; padding: 0px 4px; display: inline-block; From de26378f1c0c9605b7221c4ad959aec416dda2d4 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sun, 26 Feb 2017 12:19:59 +0100 Subject: [PATCH 036/255] Adjust HTML table style (GH2209) --- notebook/static/notebook/less/renderedhtml.less | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/notebook/static/notebook/less/renderedhtml.less b/notebook/static/notebook/less/renderedhtml.less index bdc8cb07a..4b535c7f9 100644 --- a/notebook/static/notebook/less/renderedhtml.less +++ b/notebook/static/notebook/less/renderedhtml.less @@ -74,11 +74,9 @@ text-align: right; vertical-align: middle; padding: 0.5em 0.5em; - line-height: 1.0; - white-space: nowrap; - max-width: 150px; - text-overflow: ellipsis; - overflow: hidden; + line-height: normal; + white-space: normal; + max-width: none; border: none; } th { From 78162036651ff31a77fe14b74c5d4087c322936d Mon Sep 17 00:00:00 2001 From: Peter Parente Date: Sun, 26 Feb 2017 21:51:51 -0500 Subject: [PATCH 037/255] Note bundler API, kernel activity additions --- docs/source/changelog.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 64f8ac054..b3424247b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -111,9 +111,17 @@ Other additions (:ghpull:`1837`). - Load server extensions with ConfigManager so that merge happens recursively, - unlike normal config values, to make it load more consistently with frontend + unlike normal config values, to make it load more consistently with frontend extensions(:ghpull:`2108`). +- The notebook server now supports the `bundler API + `__ + from the `jupyter_cms incubator project + `__ (:ghpull:`1579`). + +- The notebook server now provides information about kernel activity in + its kernel resource API (:ghpull:`1827`). + Remember that upgrading ``notebook`` only affects the user interface. Upgrading kernels and libraries may also provide new features, better stability and integration with the notebook interface. From 8014f1c15fe2883a57245485ea4b2267b47c99b9 Mon Sep 17 00:00:00 2001 From: Louise Davies Date: Mon, 27 Feb 2017 15:57:40 +0000 Subject: [PATCH 038/255] Updated docs on installed phantomjs --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a3a8c3b53..3b25ca002 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -124,7 +124,7 @@ JavaScript Tests To run the JavaScript tests, you will need to have PhantomJS and CasperJS installed:: - npm install -g casperjs phantomjs@1.9.18 + npm install -g casperjs phantomjs-prebuilt Then, to run the JavaScript tests:: From b2d048a85edd0e1e664ba15183fdbf1a06797ef6 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 27 Feb 2017 16:41:19 +0000 Subject: [PATCH 039/255] [WIP] Show server root directory in move dialog --- notebook/notebookapp.py | 1 + notebook/static/tree/js/notebooklist.js | 10 +++++++++- notebook/templates/tree.html | 1 + notebook/tree/handlers.py | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 498405a6d..9ae3ca923 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -255,6 +255,7 @@ class NotebookWebApplication(web.Application): mathjax_config=jupyter_app.mathjax_config, config=jupyter_app.config, config_dir=jupyter_app.config_dir, + server_root_dir=jupyter_app.notebook_dir, jinja2_env=env, terminals_available=False, # Set later if terminals are available ) diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index 94525e6d6..12e44b139 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -842,7 +842,15 @@ define([ .text('Enter new destination directory path for '+ num_items + ' items:') ).append( $("
") - ).append(input); + ).append( + $("").append( + $("").append( + $("
").text(utils.get_body_data('serverRoot')) + ).append( + $("").append(input) + ) + ).css('width', '100%') + ); var d = dialog.modal({ title : "Move "+ num_items + " Items", body : dialog_body, diff --git a/notebook/templates/tree.html b/notebook/templates/tree.html index 50e084bc1..ee49e2fb2 100644 --- a/notebook/templates/tree.html +++ b/notebook/templates/tree.html @@ -8,6 +8,7 @@ data-base-url="{{base_url | urlencode}}" data-notebook-path="{{notebook_path | urlencode}}" data-terminals-available="{{terminals_available}}" +data-server-root="{{server_root}}" {% endblock %} diff --git a/notebook/tree/handlers.py b/notebook/tree/handlers.py index 6734d3b50..63fb37cd2 100644 --- a/notebook/tree/handlers.py +++ b/notebook/tree/handlers.py @@ -49,6 +49,7 @@ class TreeHandler(IPythonHandler): notebook_path=path, breadcrumbs=breadcrumbs, terminals_available=self.settings['terminals_available'], + server_root=self.settings['server_root_dir'], )) elif cm.file_exists(path): # it's not a directory, we have redirecting to do From e751e1ca1e69123d4df472c56f91264d6524fc08 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 27 Feb 2017 09:54:23 -0800 Subject: [PATCH 040/255] Implement discussed behavior: - replace regexes they were not matching at the end of files. - hide edit/view for multiple, they where not working From the Pr comments that should closely match the following Observations: - "View" is handled by the browser so in most case it's something "safe". I think that (A) all items we don't know what to do with should have the _possibility_ to be viewed. (example `config.yaml`, `cat.gifv`). - "Edit" make sens only on text files only. In general (B) "edit" should not be the default action for most files. - Though (C) "Edit" should be triggerable by advanced users (unless we know we should not). Example `foo.zorblax`, `conf.toml` - (D) "Edit" does not make sens for some filetypes, thus should not be available (`'png`, `.jpeg`) Question (E): - If the default action is X {in edit/view} should the X button be shown ? Proposition: - if ipynb: - default link edit - no button in toolbar. - If known type, editable (txt, json, yaml, py, rb): - default link open edit, - button in toolbar show view - if known type not editable: (, png, gif, zip) - default link open view - no button in toolbar - if unknown type ('.zorblax, toml, cson, ') - default link open "view" - button in the toolbar show "Edit". --- notebook/static/tree/js/notebooklist.js | 73 +++++++++++++++++++++---- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index 29192d3c7..ff146f3fe 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -12,6 +12,26 @@ define([ ], function($, IPython, utils, dialog, events, keyboard, moment) { "use strict"; + var extension = function(path){ + /** + * return the last pat after the dot in a filepath + * or the filepath itself if no dots present. + * Empty string if the filepath ends with a dot. + **/ + var parts = path.split('.'); + return parts[parts.length-1]; + }; + + var extension_in = function(extension, extensionslist){ + var res = extensionslist.indexOf(extension) != -1; + return res; + + }; + + var filepath_of_extension = function(filepath, extensionslist){ + return extension_in(extension(filepath), extensionslist); + }; + var NotebookList = function (selector, options) { /** * Constructor @@ -515,6 +535,21 @@ define([ this._selection_changed(); }; + NotebookList.ipynb_extensions = ['ipynb']; + NotebookList.non_editable_extensions = 'jpeg jpeg png zip gif tif tiff bmp ico pdf doc xls xlsx'.split(' '); + NotebookList.editable_extensions = 'txt py cson json yaml html'.split(' '); + + NotebookList.prototype._is_editable = function(filepath){ + return filepath_of_extension(filepath, NotebookList.editable_extensions); + }; + + NotebookList.prototype._is_not_editable = function(filepath){ + return filepath_of_extension(filepath, NotebookList.non_editable_extensions); + }; + + NotebookList.prototype._is_notebook = function(filepath){ + return filepath_of_extension(filepath, NotebookList.ipynb_extensions) + }; /** * Handles when any row selector checkbox is toggled. @@ -523,6 +558,7 @@ define([ // Use a JQuery selector to find each row with a checked checkbox. If // we decide to add more checkboxes in the future, this code will need // to be changed to distinguish which checkbox is the row selector. + var that = this; var selected = []; var has_running_notebook = false; var has_directory = false; @@ -599,18 +635,33 @@ define([ $('.delete-button').css('display', 'none'); } - // View is visible when an item is renderable or downloadable - if (selected.length > 0 && !has_directory && selected.every(function(el) { - return el.path.match(/html?/); + // View is visible in the following case: + // + // - the item is editable + // - it is not a notebook + // + // If it's not editable or unknown, the default action should be view + // already so no need to show the button. + // That should include things like, html, py, txt, json.... + if (selected.length == 1 && !has_directory && selected.every(function(el) { + return that._is_editable(el.path) && ! that._is_notebook(el.path); })) { $('.view-button').css('display', 'inline-block'); } else { $('.view-button').css('display', 'none'); } - // Edit is visible when an item is editable - if (selected.length > 0 && !has_directory && !selected.find(function(el) { - return el.path.match(/ipynb|jpe?g|png|gif|tiff?|bmp|ico|pdf|doc|xls/); + // Edit is visible when an item is unknown, that is to say: + // - not in the editable list + // - not in the known non-editable list. + // - not a notebook. + // Indeed if it's editable the default action is already to edit. + // And non editable files should not show edit button. + // for unknown we'll assume users know what they are doing. + if (selected.length == 1 && !has_directory && selected.find(function(el) { + return !that._is_editable(el.path) + && !that._is_not_editable(el.path) + && !that._is_notebook(el.path); })) { $('.edit-button').css('display', 'inline-block'); } else { @@ -671,12 +722,10 @@ define([ icon = 'running_' + icon; } var uri_prefix = NotebookList.uri_prefixes[model.type]; - if (model.type === 'file' && - model.mimetype && - model.mimetype.substr(0, 5) !== 'text/' && - this.EDIT_MIMETYPES.indexOf(model.mimetype) < 0) { - // send text/unidentified files to editor, others go to raw viewer - uri_prefix = 'files'; + if (model.type === 'file' + && !this._is_editable(path)) + { + uri_prefix = 'view'; } item.find(".item_icon").addClass(icon).addClass('icon-fixed-width'); From 3c40901b3484172f748cd85ec04a489fb1c1b085 Mon Sep 17 00:00:00 2001 From: Lorenzo Gasparini Date: Wed, 1 Mar 2017 15:29:02 +0100 Subject: [PATCH 041/255] Add support for '\\[' and '\\]' as math delimiters, refactoring. --- notebook/static/notebook/js/mathjaxutils.js | 29 ++++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/notebook/static/notebook/js/mathjaxutils.js b/notebook/static/notebook/js/mathjaxutils.js index fa24a93d2..d55255fde 100644 --- a/notebook/static/notebook/js/mathjaxutils.js +++ b/notebook/static/notebook/js/mathjaxutils.js @@ -59,7 +59,7 @@ define([ // MATHSPLIT contains the pattern for math delimiters and special symbols // needed for searching for math in the text input. - var MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)))/i; + var MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)|\[|\]))/i; // The math is in blocks i through j, so // collect it into one block and clear the others. @@ -178,10 +178,10 @@ define([ end = block; braces = 0; } - else if (block === "\\\\\(") { - start = i; - end = "\\\\\)"; - braces = 0; + else if (block === "\\\\\(" || block === "\\\\\[") { + start = i; + end = block.slice(-1) === "(" ? "\\\\\)" : "\\\\\]"; + braces = 0; } else if (block.substr(1, 5) === "begin") { start = i; @@ -204,11 +204,20 @@ define([ // and clear the math array (no need to keep it around). // var replace_math = function (text, math) { - text = text.replace(/@@(\d+)@@/g, function (match, n) { - return math[n] - .replace("\\\\\(", "\\\(") - .replace("\\\\\)", "\\\)"); - }); + var math_group_process = function (match, n) { + var math_group = math[n]; + + if (math_group.substr(0, 3) === "\\\\\(" && math_group.substr(math_group.length - 3) === "\\\\\)") { + math_group = "\\\(" + math_group.substring(3, math_group.length - 3) + "\\\)"; + } else if (math_group.substr(0, 3) === "\\\\\[" && math_group.substr(math_group.length - 3) === "\\\\\]") { + math_group = "\\\[" + math_group.substring(3, math_group.length - 3) + "\\\]"; + } + + return math_group; + }; + + text = text.replace(/@@(\d+)@@/g, math_group_process); + return text; }; From 3b50d4b4ad8bd0df68876bd60186b86a41ccc857 Mon Sep 17 00:00:00 2001 From: Lorenzo Gasparini Date: Wed, 1 Mar 2017 18:53:34 +0100 Subject: [PATCH 042/255] Add comments --- notebook/static/notebook/js/mathjaxutils.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/notebook/static/notebook/js/mathjaxutils.js b/notebook/static/notebook/js/mathjaxutils.js index d55255fde..b68fa8522 100644 --- a/notebook/static/notebook/js/mathjaxutils.js +++ b/notebook/static/notebook/js/mathjaxutils.js @@ -204,6 +204,11 @@ define([ // and clear the math array (no need to keep it around). // var replace_math = function (text, math) { + // + // Replaces a math placeholder with its corresponding group. + // The math delimiters "\\(", "\\[", "\\)" and "\\]" are replaced + // removing one backslash in order to be interpreted correctly by MathJax. + // var math_group_process = function (match, n) { var math_group = math[n]; @@ -216,6 +221,8 @@ define([ return math_group; }; + // Replace all the math group placeholders in the text + // with the saved strings. text = text.replace(/@@(\d+)@@/g, math_group_process); return text; From 44a1e99f2a40b84c8a00fabb4ec7fb3ba7a680da Mon Sep 17 00:00:00 2001 From: Michael Pacer Date: Wed, 1 Mar 2017 11:33:39 -0800 Subject: [PATCH 043/255] Remove misleading local variable declaration --- notebook/static/notebook/js/mathjaxutils.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/notebook/static/notebook/js/mathjaxutils.js b/notebook/static/notebook/js/mathjaxutils.js index b68fa8522..bd82eea73 100644 --- a/notebook/static/notebook/js/mathjaxutils.js +++ b/notebook/static/notebook/js/mathjaxutils.js @@ -55,8 +55,6 @@ define([ // Other minor modifications are also due to StackExchange and are used with // permission. - var inline = "$"; // the inline math delimiter - // MATHSPLIT contains the pattern for math delimiters and special symbols // needed for searching for math in the text input. var MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)|\[|\]))/i; @@ -173,7 +171,7 @@ define([ // Look for math start delimiters and when // found, set up the end delimiter. // - if (block === inline || block === "$$") { + if (block === "$" || block === "$$") { start = i; end = block; braces = 0; From c782cb327fad9e27527202ee12debe35bd8272bc Mon Sep 17 00:00:00 2001 From: Lorenzo Gasparini Date: Wed, 1 Mar 2017 23:38:31 +0100 Subject: [PATCH 044/255] Swap $ and $$ delimiters order to avoid conflicts. --- notebook/static/notebook/js/codemirror-ipythongfm.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/notebook/static/notebook/js/codemirror-ipythongfm.js b/notebook/static/notebook/js/codemirror-ipythongfm.js index 9a6bbc36b..fb2fb60df 100644 --- a/notebook/static/notebook/js/codemirror-ipythongfm.js +++ b/notebook/static/notebook/js/codemirror-ipythongfm.js @@ -33,14 +33,15 @@ return CodeMirror.multiplexingMode( gfm_mode, + // By defining the $$ delimiter before the $ delimiter we don't run + // into the problem that $$ is interpreted as two consecutive $. { - open: "$", close: "$", + open: "$$", close: "$$", mode: tex_mode, delimStyle: "delimit" }, { - // not sure this works as $$ is interpreted at (opening $, closing $, as defined just above) - open: "$$", close: "$$", + open: "$", close: "$", mode: tex_mode, delimStyle: "delimit" }, From 7d4965abc0cc2691ef16dd77b448064de67a83e1 Mon Sep 17 00:00:00 2001 From: Kaushik Chaubal Date: Wed, 8 Feb 2017 11:53:20 -0500 Subject: [PATCH 045/255] fix of uploading large files crashes the browser #96 + adding unit tests --- notebook/notebookapp.py | 3 +- notebook/services/contents/handlers.py | 4 +- .../services/contents/largefilemanager.py | 73 +++++++ .../contents/tests/test_largefilemanager.py | 113 +++++++++++ notebook/static/tree/js/notebooklist.js | 192 +++++++++++++++++- 5 files changed, 377 insertions(+), 8 deletions(-) create mode 100644 notebook/services/contents/largefilemanager.py create mode 100644 notebook/services/contents/tests/test_largefilemanager.py diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 498405a6d..4b6a98dd8 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -75,6 +75,7 @@ from .services.kernels.kernelmanager import MappingKernelManager from .services.config import ConfigManager from .services.contents.manager import ContentsManager from .services.contents.filemanager import FileContentsManager +from .services.contents.largefilemanager import LargeFileManager from .services.sessions.sessionmanager import SessionManager from .auth.login import LoginHandler @@ -843,7 +844,7 @@ class NotebookApp(JupyterApp): self.log.info("Using MathJax configuration file: %s", change['new']) contents_manager_class = Type( - default_value=FileContentsManager, + default_value=LargeFileManager, klass=ContentsManager, config=True, help='The notebook manager class to use.' diff --git a/notebook/services/contents/handlers.py b/notebook/services/contents/handlers.py index d4e528abd..640309065 100644 --- a/notebook/services/contents/handlers.py +++ b/notebook/services/contents/handlers.py @@ -176,7 +176,9 @@ class ContentsHandler(APIHandler): @gen.coroutine def _save(self, model, path): """Save an existing file.""" - self.log.info(u"Saving file at %s", path) + chunk = model.get("chunk", None) + if not chunk or chunk == -1: # Avoid tedious log information + self.log.info(u"Saving file at %s", path) model = yield gen.maybe_future(self.contents_manager.save(model, path)) validate_model(model, expect_content=False) self._finish_model(model) diff --git a/notebook/services/contents/largefilemanager.py b/notebook/services/contents/largefilemanager.py new file mode 100644 index 000000000..c00f243c1 --- /dev/null +++ b/notebook/services/contents/largefilemanager.py @@ -0,0 +1,73 @@ +from notebook.services.contents.filemanager import FileContentsManager +from contextlib import contextmanager +from tornado import web +import nbformat +import base64 +import os, io + +class LargeFileManager(FileContentsManager): + """Handle large file upload.""" + + def save(self, model, path=''): + """Save the file model and return the model with no content.""" + chunk = model.get('chunk', None) + if chunk is not None: + path = path.strip('/') + + if 'type' not in model: + raise web.HTTPError(400, u'No file type provided') + if model['type'] != 'file': + raise web.HTTPError(400, u'File type "{}" is not supported for large file transfer'.format(model['type'])) + if 'content' not in model and model['type'] != 'directory': + raise web.HTTPError(400, u'No file content provided') + + os_path = self._get_os_path(path) + + # First chunk + if chunk == 1: + self.log.debug("Saving %s", os_path) + self.run_pre_save_hook(model=model, path=path) + try: + # First chunk + if chunk == 1: + super(LargeFileManager, self)._save_file(os_path, model['content'], model.get('format')) + else: + self._save_large_file(os_path, model['content'], model.get('format')) + except web.HTTPError: + raise + except Exception as e: + self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True) + raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e)) + + model = self.get(path, content=False) + + # Last chunk + if chunk == -1: + self.run_post_save_hook(model=model, os_path=os_path) + return model + else: + return super(LargeFileManager, self).save(model, path) + + def _save_large_file(self, os_path, content, format): + """Save content of a generic file.""" + if format not in {'text', 'base64'}: + raise web.HTTPError( + 400, + "Must specify format of file contents as 'text' or 'base64'", + ) + try: + if format == 'text': + bcontent = content.encode('utf8') + else: + b64_bytes = content.encode('ascii') + bcontent = base64.decodestring(b64_bytes) + except Exception as e: + raise web.HTTPError( + 400, u'Encoding error saving %s: %s' % (os_path, e) + ) + + with self.perm_to_403(os_path): + if os.path.islink(os_path): + os_path = os.path.join(os.path.dirname(os_path), os.readlink(os_path)) + with io.open(os_path, 'ab') as f: + f.write(bcontent) diff --git a/notebook/services/contents/tests/test_largefilemanager.py b/notebook/services/contents/tests/test_largefilemanager.py new file mode 100644 index 000000000..13d294b9b --- /dev/null +++ b/notebook/services/contents/tests/test_largefilemanager.py @@ -0,0 +1,113 @@ +from unittest import TestCase +from ipython_genutils.tempdir import TemporaryDirectory +from ..largefilemanager import LargeFileManager +import os +from tornado import web + + +def _make_dir(contents_manager, api_path): + """ + Make a directory. + """ + os_path = contents_manager._get_os_path(api_path) + try: + os.makedirs(os_path) + except OSError: + print("Directory already exists: %r" % os_path) + + +class TestLargeFileManager(TestCase): + + def setUp(self): + self._temp_dir = TemporaryDirectory() + self.td = self._temp_dir.name + self.contents_manager = LargeFileManager(root_dir=self.td) + + def make_dir(self, api_path): + """make a subdirectory at api_path + + override in subclasses if contents are not on the filesystem. + """ + _make_dir(self.contents_manager, api_path) + + def test_save(self): + + cm = self.contents_manager + # Create a notebook + model = cm.new_untitled(type='notebook') + name = model['name'] + path = model['path'] + + # Get the model with 'content' + full_model = cm.get(path) + # Save the notebook + model = cm.save(full_model, path) + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], name) + self.assertEqual(model['path'], path) + + try: + model = {'name': 'test', 'path': 'test', 'chunk': 1} + cm.save(model, model['path']) + except web.HTTPError as e: + self.assertEqual('HTTP 400: Bad Request (No file type provided)', str(e)) + + try: + model = {'name': 'test', 'path': 'test', 'chunk': 1, 'type': 'notebook'} + cm.save(model, model['path']) + except web.HTTPError as e: + self.assertEqual('HTTP 400: Bad Request (File type "notebook" is not supported for large file transfer)', str(e)) + + try: + model = {'name': 'test', 'path': 'test', 'chunk': 1, 'type': 'file'} + cm.save(model, model['path']) + except web.HTTPError as e: + self.assertEqual('HTTP 400: Bad Request (No file content provided)', str(e)) + + try: + model = {'name': 'test', 'path': 'test', 'chunk': 2, 'type': 'file', + 'content': u'test', 'format': 'json'} + cm.save(model, model['path']) + except web.HTTPError as e: + self.assertEqual("HTTP 400: Bad Request (Must specify format of file contents as 'text' or 'base64')", + str(e)) + + # Save model for different chunks + model = {'name': 'test', 'path': 'test', 'type': 'file', + 'content': u'test==', 'format': 'text'} + name = model['name'] + path = model['path'] + cm.save(model, path) + + for chunk in (1, 2, -1): + for fm in ('text', 'base64'): + full_model = cm.get(path) + full_model['chunk'] = chunk + full_model['format'] = fm + model_res = cm.save(full_model, path) + assert isinstance(model_res, dict) + + self.assertIn('name', model_res) + self.assertIn('path', model_res) + self.assertNotIn('chunk', model_res) + self.assertEqual(model_res['name'], name) + self.assertEqual(model_res['path'], path) + + # Test in sub-directory + # Create a directory and notebook in that directory + sub_dir = '/foo/' + self.make_dir('foo') + model = cm.new_untitled(path=sub_dir, type='notebook') + name = model['name'] + path = model['path'] + model = cm.get(path) + + # Change the name in the model for rename + model = cm.save(model, path) + assert isinstance(model, dict) + self.assertIn('name', model) + self.assertIn('path', model) + self.assertEqual(model['name'], 'Untitled.ipynb') + self.assertEqual(model['path'], 'foo/Untitled.ipynb') diff --git a/notebook/static/tree/js/notebooklist.js b/notebook/static/tree/js/notebooklist.js index ff146f3fe..db4c1662b 100644 --- a/notebook/static/tree/js/notebooklist.js +++ b/notebook/static/tree/js/notebooklist.js @@ -278,13 +278,9 @@ define([ var name_and_ext = utils.splitext(f.name); var file_ext = name_and_ext[1]; - // skip large files with a warning + // replace the error message to the new upload function if (f.size > this._max_upload_size_mb * 1024 * 1024) { - dialog.modal({ - title : 'Cannot upload file', - body : "Cannot upload file (>" + this._max_upload_size_mb + " MB) '" + f.name + "'", - buttons : {'OK' : { 'class' : 'btn-primary' }} - }); + that.add_large_file_upload_button(f); continue; } @@ -1085,6 +1081,190 @@ define([ }); }; + // Add a new class for large file upload + NotebookList.prototype.add_large_file_upload_button = function (file) { + var that = this; + var item = that.new_item(0, true); + item.addClass('new-file'); + that.add_name_input(file.name, item, 'file'); + var cancel_button = $('