@@ -299,19 +298,25 @@ class="notebook_app"
diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 2a313bab6..2ca1f41c5 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -611,9 +611,9 @@ class FileContentsManager(ContentsManager): return "Serving notebooks from local directory: %s" % self.root_dir def get_kernel_path(self, path, model=None): - """Return the initial working dir a kernel associated with a given notebook""" + """Return the initial API path of a kernel associated with a given notebook""" if '/' in path: parent_dir = path.rsplit('/', 1)[0] else: parent_dir = '' - return self._get_os_path(parent_dir) + return parent_dir diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index 491401f79..1c87064ca 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -187,8 +187,12 @@ class ContentsManager(LoggingConfigurable): KernelManagers can turn this value into a filesystem path, or ignore it altogether. + + The default value here will start kernels in the directory of the + notebook server. FileContentsManager overrides this to use the + directory containing the notebook. """ - return path + return '' def increment_filename(self, filename, path='', insert=''): """Increment a filename until it is unique. diff --git a/IPython/html/services/kernels/kernelmanager.py b/IPython/html/services/kernels/kernelmanager.py index e1bd5c256..db73aa4c3 100644 --- a/IPython/html/services/kernels/kernelmanager.py +++ b/IPython/html/services/kernels/kernelmanager.py @@ -54,14 +54,10 @@ class MappingKernelManager(MultiKernelManager): def cwd_for_path(self, path): """Turn API path into absolute OS path.""" - # short circuit for NotebookManagers that pass in absolute paths - if os.path.exists(path): - return path - os_path = to_os_path(path, self.root_dir) # in the case of notebooks and kernels not being on the same filesystem, # walk up to root_dir if the paths don't exist - while not os.path.exists(os_path) and os_path != self.root_dir: + while not os.path.isdir(os_path) and os_path != self.root_dir: os_path = os.path.dirname(os_path) return os_path diff --git a/IPython/html/services/kernelspecs/handlers.py b/IPython/html/services/kernelspecs/handlers.py index f8104147d..2d1a75fc2 100644 --- a/IPython/html/services/kernelspecs/handlers.py +++ b/IPython/html/services/kernelspecs/handlers.py @@ -7,8 +7,6 @@ from tornado import web from ...base.handlers import IPythonHandler, json_errors -from IPython.kernel.kernelspec import _pythonfirst - class MainKernelSpecHandler(IPythonHandler): SUPPORTED_METHODS = ('GET',) @@ -17,18 +15,21 @@ class MainKernelSpecHandler(IPythonHandler): @json_errors def get(self): ksm = self.kernel_spec_manager - results = [] - for kernel_name in sorted(ksm.find_kernel_specs(), key=_pythonfirst): + km = self.kernel_manager + model = {} + model['default'] = km.default_kernel_name + model['kernelspecs'] = specs = {} + for kernel_name in ksm.find_kernel_specs(): try: d = ksm.get_kernel_spec(kernel_name).to_dict() except Exception: self.log.error("Failed to load kernel spec: '%s'", kernel_name, exc_info=True) continue d['name'] = kernel_name - results.append(d) + specs[kernel_name] = d self.set_header("Content-Type", 'application/json') - self.finish(json.dumps(results)) + self.finish(json.dumps(model)) class KernelSpecHandler(IPythonHandler): diff --git a/IPython/html/services/kernelspecs/tests/test_kernelspecs_api.py b/IPython/html/services/kernelspecs/tests/test_kernelspecs_api.py index a82a751b5..0d43fb1de 100644 --- a/IPython/html/services/kernelspecs/tests/test_kernelspecs_api.py +++ b/IPython/html/services/kernelspecs/tests/test_kernelspecs_api.py @@ -78,16 +78,22 @@ class APITest(NotebookTestBase): with open(pjoin(bad_kernel_dir, 'kernel.json'), 'w') as f: f.write("garbage") - specs = self.ks_api.list().json() - assert isinstance(specs, list) + model = self.ks_api.list().json() + assert isinstance(model, dict) + self.assertEqual(model['default'], NATIVE_KERNEL_NAME) + specs = model['kernelspecs'] + assert isinstance(specs, dict) # 2: the sample kernelspec created in setUp, and the native Python kernel self.assertGreaterEqual(len(specs), 2) shutil.rmtree(bad_kernel_dir) def test_list_kernelspecs(self): - specs = self.ks_api.list().json() - assert isinstance(specs, list) + model = self.ks_api.list().json() + assert isinstance(model, dict) + self.assertEqual(model['default'], NATIVE_KERNEL_NAME) + specs = model['kernelspecs'] + assert isinstance(specs, dict) # 2: the sample kernelspec created in setUp, and the native Python kernel self.assertGreaterEqual(len(specs), 2) @@ -98,8 +104,8 @@ class APITest(NotebookTestBase): def is_default_kernelspec(s): return s['name'] == NATIVE_KERNEL_NAME and s['display_name'].startswith("IPython") - assert any(is_sample_kernelspec(s) for s in specs), specs - assert any(is_default_kernelspec(s) for s in specs), specs + assert any(is_sample_kernelspec(s) for s in specs.values()), specs + assert any(is_default_kernelspec(s) for s in specs.values()), specs def test_get_kernelspec(self): spec = self.ks_api.kernel_spec_info('Sample').json() # Case insensitive diff --git a/IPython/html/static/base/js/page.js b/IPython/html/static/base/js/page.js index 7c2528564..52e89b7e6 100644 --- a/IPython/html/static/base/js/page.js +++ b/IPython/html/static/base/js/page.js @@ -4,14 +4,17 @@ define([ 'base/js/namespace', 'jquery', -], function(IPython, $){ + 'base/js/events', +], function(IPython, $, events){ "use strict"; var Page = function () { this.bind_events(); + this._resize_header(); }; Page.prototype.bind_events = function () { + events.on('resize-header.Page', $.proxy(this._resize_header, this)); }; Page.prototype.show = function () { @@ -41,6 +44,11 @@ define([ $('div#site').css('display','block'); }; + Page.prototype._resize_header = function() { + // Update the header's size. + $('#header-spacer').height($('#header').height()); + }; + // Register self in the global namespace for convenience. IPython.Page = Page; return {'Page': Page}; diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index ce4ed4b6e..6d4214143 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -541,7 +541,16 @@ define([ if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux"; return OSName; })(); - + + var get_url_param = function (name) { + // get a URL parameter. I cannot believe we actually need this. + // Based on http://stackoverflow.com/a/25359264/938949 + var match = new RegExp('[\?&]' + name + '=([^&]*)').exec(window.location.search); + if (match){ + return decodeURIComponent(match[1] || ''); + } + }; + var is_or_has = function (a, b) { /** * Is b a child of a or a itself? @@ -632,6 +641,7 @@ define([ * Like $.ajax, but returning an ES6 promise. success and error settings * will be ignored. */ + settings = settings || {}; return new Promise(function(resolve, reject) { settings.success = function(data, status, jqXHR) { resolve(data); @@ -766,6 +776,33 @@ define([ }; }; + var typeset = function(element, text) { + /** + * Apply MathJax rendering to an element, and optionally set its text + * + * If MathJax is not available, make no changes. + * + * Returns the output any number of typeset elements, or undefined if + * MathJax was not available. + * + * Parameters + * ---------- + * element: Node, NodeList, or jQuery selection + * text: option string + */ + if(!window.MathJax){ + return; + } + var $el = element.jquery ? element : $(element); + if(arguments.length > 1){ + $el.text(text); + } + return $el.map(function(){ + // MathJax takes a DOM node: $.map makes `this` the context + return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]); + }); + }; + var utils = { regex_split : regex_split, uuid : uuid, @@ -786,6 +823,7 @@ define([ from_absolute_cursor_pos : from_absolute_cursor_pos, browser : browser, platform: platform, + get_url_param: get_url_param, is_or_has : is_or_has, is_focused : is_focused, mergeopt: mergeopt, @@ -799,6 +837,7 @@ define([ load_class: load_class, resolve_promises_dict: resolve_promises_dict, reject: reject, + typeset: typeset, }; // Backwards compatability. diff --git a/IPython/html/static/base/less/page.less b/IPython/html/static/base/less/page.less index 772a4bbf4..baa93a166 100644 --- a/IPython/html/static/base/less/page.less +++ b/IPython/html/static/base/less/page.less @@ -20,11 +20,33 @@ body { div#header { /* Initially hidden to prevent FLOUC */ display: none; - margin-bottom: 0px; - padding-left: 30px; - padding-bottom: 5px; - border-bottom: 1px solid @navbar-default-border; - .border-box-sizing(); + margin-bottom: -6px; + position: fixed; + top: 0; + width: 100%; + background-color: @body-bg; + min-height: 31px; + + /* Display over codemirror */ + z-index: 100; + + #header-container { + margin-bottom: 0px; + padding-left: 30px; + padding-bottom: 5px; + .border-box-sizing(); + } + + .header-bar { + width: 100%; + height: 0px; + border-bottom: 1px solid @navbar-default-border; + } +} + +#header-spacer { + width: 100%; + visibility: hidden; } #ipython_notebook { diff --git a/IPython/html/static/notebook/js/cell.js b/IPython/html/static/notebook/js/cell.js index b122c2aa3..9c1daa8d2 100644 --- a/IPython/html/static/notebook/js/cell.js +++ b/IPython/html/static/notebook/js/cell.js @@ -220,10 +220,7 @@ define([ * @method typeset */ Cell.prototype.typeset = function () { - if (window.MathJax) { - var cell_math = this.element.get(0); - MathJax.Hub.Queue(["Typeset", MathJax.Hub, cell_math]); - } + utils.typeset(this.element); }; /** diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index 497266f09..d4a9e22ff 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -389,7 +389,7 @@ define([ * @private */ CodeCell.prototype._handle_set_next_input = function (payload) { - var data = {'cell': this, 'text': payload.text}; + var data = {'cell': this, 'text': payload.text, replace: payload.replace}; this.events.trigger('set_next_input.Notebook', data); }; diff --git a/IPython/html/static/notebook/js/kernelselector.js b/IPython/html/static/notebook/js/kernelselector.js index 00da81928..46d488289 100644 --- a/IPython/html/static/notebook/js/kernelselector.js +++ b/IPython/html/static/notebook/js/kernelselector.js @@ -25,16 +25,27 @@ define([ KernelSelector.prototype.request_kernelspecs = function() { var url = utils.url_join_encode(this.notebook.base_url, 'api/kernelspecs'); - $.ajax(url, {success: $.proxy(this._got_kernelspecs, this)}); + utils.promising_ajax(url).then($.proxy(this._got_kernelspecs, this)); }; - KernelSelector.prototype._got_kernelspecs = function(data, status, xhr) { - this.kernelspecs = {}; + KernelSelector.prototype._got_kernelspecs = function(data) { + this.kernelspecs = data.kernelspecs; var menu = this.element.find("#kernel_selector"); var change_kernel_submenu = $("#menu-change-kernel-submenu"); - for (var i = 0; i < data.length; i++) { - var ks = data[i]; - this.kernelspecs[ks.name] = ks; + var keys = Object.keys(data.kernelspecs).sort(function (a, b) { + // sort by display_name + var da = data.kernelspecs[a].display_name; + var db = data.kernelspecs[b].display_name; + if (da === db) { + return 0; + } else if (da > db) { + return 1; + } else { + return -1; + } + }); + for (var i = 0; i < keys.length; i++) { + var ks = this.kernelspecs[keys[i]]; var ksentry = $("