diff --git a/bower.json b/bower.json index e58ec9279..7ce891988 100644 --- a/bower.json +++ b/bower.json @@ -7,7 +7,7 @@ "bootstrap-tour": "0.9.0", "codemirror": "components/codemirror#~5.22.2", "es6-promise": "~1.0", - "font-awesome": "components/font-awesome#~4.2.0", + "font-awesome": "components/font-awesome#~4.7.0", "google-caja": "5669", "jquery": "components/jquery#~2.0", "jquery-typeahead": "~2.0.0", diff --git a/docs/source/security.rst b/docs/source/security.rst index 4c57ef0f5..f90f9d6da 100644 --- a/docs/source/security.rst +++ b/docs/source/security.rst @@ -31,7 +31,8 @@ When you start a notebook server with token authentication enabled (default), a token is generated to use for authentication. This token is logged to the terminal, so that you can copy/paste the URL into your browser:: - [I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01 + [I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at: + http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01 If the notebook server is going to open your browser automatically diff --git a/notebook/_sysinfo.py b/notebook/_sysinfo.py index ea237bbd2..4e6a36626 100644 --- a/notebook/_sysinfo.py +++ b/notebook/_sysinfo.py @@ -46,11 +46,15 @@ def pkg_commit_hash(pkg_path): while cur_path != par_path: cur_path = par_path if p.exists(p.join(cur_path, '.git')): - proc = subprocess.Popen('git rev-parse --short HEAD', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=pkg_path, shell=True) - repo_commit, _ = proc.communicate() + try: + proc = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=pkg_path) + repo_commit, _ = proc.communicate() + except OSError: + repo_commit = None + if repo_commit: return 'repository', repo_commit.strip().decode('ascii') else: diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 2c48d5cc1..cf3e183f0 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -39,7 +39,12 @@ from notebook.services.security import csp_report_uri #----------------------------------------------------------------------------- non_alphanum = re.compile(r'[^A-Za-z0-9]') -sys_info = json.dumps(get_sys_info()) +_sys_info_cache = None +def json_sys_info(): + global _sys_info_cache + if _sys_info_cache is None: + _sys_info_cache = json.dumps(get_sys_info()) + return _sys_info_cache def log(): if Application.initialized(): @@ -357,7 +362,7 @@ class IPythonHandler(AuthenticatedHandler): login_available=self.login_available, token_available=bool(self.token or self.one_time_token), static_url=self.static_url, - sys_info=sys_info, + sys_info=json_sys_info(), contents_js_source=self.contents_js_source, version_hash=self.version_hash, ignore_minified_js=self.ignore_minified_js, diff --git a/notebook/base/zmqhandlers.py b/notebook/base/zmqhandlers.py index 66e31eb18..8878948da 100644 --- a/notebook/base/zmqhandlers.py +++ b/notebook/base/zmqhandlers.py @@ -296,5 +296,4 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler): self.session = Session(config=self.config) def get_compression_options(self): - # use deflate compress websocket - return {} + return self.settings.get('websocket_compression_options', None) diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 839102cdd..934aaa099 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -93,7 +93,7 @@ from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel, NATIVE_KE from jupyter_client.session import Session from nbformat.sign import NotebookNotary from traitlets import ( - Dict, Unicode, Integer, List, Bool, Bytes, Instance, + Any, Dict, Unicode, Integer, List, Bool, Bytes, Instance, TraitError, Type, Float, observe, default, validate ) from ipython_genutils import py3compat @@ -294,6 +294,7 @@ class NotebookWebApplication(web.Application): handlers.extend(load_handlers('services.nbconvert.handlers')) handlers.extend(load_handlers('services.kernelspecs.handlers')) handlers.extend(load_handlers('services.security.handlers')) + handlers.extend(load_handlers('services.shutdown')) handlers.append( (r"/nbextensions/(.*)", FileFindHandler, { @@ -740,7 +741,18 @@ class NotebookApp(JupyterApp): tornado_settings = Dict(config=True, help="Supply overrides for the tornado.web.Application that the " "Jupyter notebook uses.") - + + websocket_compression_options = Any(None, config=True, + help=""" + Set the tornado compression options for websocket connections. + + This value will be returned from :meth:`WebSocketHandler.get_compression_options`. + None (default) will disable compression. + A dict (even an empty one) will enable compression. + + See the tornado docs for WebSocketHandler.get_compression_options for details. + """ + ) terminado_settings = Dict(config=True, help='Supply overrides for terminado. Currently only supports "shell_command".') @@ -1107,6 +1119,7 @@ class NotebookApp(JupyterApp): def init_webapp(self): """initialize tornado webapp and httpserver""" self.tornado_settings['allow_origin'] = self.allow_origin + self.tornado_settings['websocket_compression_options'] = self.websocket_compression_options if self.allow_origin_pat: self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat) self.tornado_settings['allow_credentials'] = self.allow_credentials @@ -1206,7 +1219,7 @@ class NotebookApp(JupyterApp): log("Terminals not available (error was %s)", e) def init_signal(self): - if not sys.platform.startswith('win') and sys.stdin.isatty(): + if not sys.platform.startswith('win') and sys.stdin and sys.stdin.isatty(): signal.signal(signal.SIGINT, self._handle_sigint) signal.signal(signal.SIGTERM, self._signal_stop) if hasattr(signal, 'SIGUSR1'): @@ -1353,7 +1366,8 @@ class NotebookApp(JupyterApp): "Return the current working directory and the server url information" info = self.contents_manager.info_string() + "\n" info += "%d active kernels \n" % len(self.kernel_manager._kernels) - return info + "The Jupyter Notebook is running at: %s" % self.display_url + # Format the info so that the URL fits on a single line in 80 char display + return info + "The Jupyter Notebook is running at:\n\r%s" % self.display_url def server_info(self): """Return a JSONable dict of information about this server.""" diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index 92a135b4f..a05253ac3 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -281,7 +281,7 @@ class FileContentsManager(FileManagerMixin, ContentsManager): if e.errno == errno.ENOENT: self.log.warning("%s doesn't exist", os_path) else: - self.log.warning("Error stat-ing %s: %s", (os_path, e)) + self.log.warning("Error stat-ing %s: %s", os_path, e) continue if not stat.S_ISREG(st.st_mode) and not stat.S_ISDIR(st.st_mode): diff --git a/notebook/services/contents/tests/test_contents_api.py b/notebook/services/contents/tests/test_contents_api.py index 439dffdfa..990b6ca51 100644 --- a/notebook/services/contents/tests/test_contents_api.py +++ b/notebook/services/contents/tests/test_contents_api.py @@ -249,8 +249,8 @@ class APITest(NotebookTestBase): self.assertEqual(nbnames, expected) nbs = notebooks_only(self.api.list('ordering').json()) - nbnames = [n['name'] for n in nbs] - expected = ['A.ipynb', 'b.ipynb', 'C.ipynb'] + nbnames = {n['name'] for n in nbs} + expected = {'A.ipynb', 'b.ipynb', 'C.ipynb'} self.assertEqual(nbnames, expected) def test_list_dirs(self): diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index e81148e10..569c60426 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -15,7 +15,7 @@ from tornado.ioloop import IOLoop, PeriodicCallback from jupyter_client.session import Session from jupyter_client.multikernelmanager import MultiKernelManager -from traitlets import Dict, List, Unicode, TraitError, Integer, default, validate +from traitlets import Bool, Dict, List, Unicode, TraitError, Integer, default, validate from notebook.utils import to_os_path from notebook._tz import utcnow, isoformat @@ -71,6 +71,16 @@ class MappingKernelManager(MultiKernelManager): help="""The interval (in seconds) on which to check for idle kernels exceeding the cull timeout value.""" ) + cull_connected = Bool(False, config=True, + help="""Whether to consider culling kernels which have one or more connections. + Only effective if cull_idle_timeout is not 0.""" + ) + + cull_busy = Bool(False, config=True, + help="""Whether to consider culling kernels which are busy. + Only effective if cull_idle_timeout is not 0.""" + ) + #------------------------------------------------------------------------- # Methods for managing kernels and sessions #------------------------------------------------------------------------- @@ -273,6 +283,10 @@ class MappingKernelManager(MultiKernelManager): 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) + if self.cull_busy: + self.log.info("Culling kernels even if busy") + if self.cull_connected: + self.log.info("Culling kernels even with connected clients") self._culler_callback.start() self._initialized_culler = True @@ -294,8 +308,15 @@ class MappingKernelManager(MultiKernelManager): 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 + # Compute idle properties + is_idle_time = dt_idle > timedelta(seconds=self.cull_idle_timeout) + is_idle_execute = self.cull_busy or (kernel.execution_state != 'busy') + connections = self._kernel_connections.get(kernel_id, 0) + is_idle_connected = self.cull_connected or not connections + # Cull the kernel if all three criteria are met + if (is_idle_time and is_idle_execute and is_idle_connected): 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.log.warning("Culling '%s' kernel '%s' (%s) with %d connections due to %s seconds of inactivity.", + kernel.execution_state, kernel.kernel_name, kernel_id, connections, idle_duration) self.shutdown_kernel(kernel_id) diff --git a/notebook/services/shutdown.py b/notebook/services/shutdown.py new file mode 100644 index 000000000..78d1f2ad6 --- /dev/null +++ b/notebook/services/shutdown.py @@ -0,0 +1,15 @@ +"""HTTP handler to shut down the notebook server. +""" +from tornado import web, ioloop +from notebook.base.handlers import IPythonHandler + +class ShutdownHandler(IPythonHandler): + @web.authenticated + def post(self): + self.log.info("Shutting down on /api/shutdown request.") + ioloop.IOLoop.current().stop() + + +default_handlers = [ + (r"/api/shutdown", ShutdownHandler), +] diff --git a/notebook/static/base/images/favicon-busy-1.ico b/notebook/static/base/images/favicon-busy-1.ico new file mode 100644 index 000000000..5b46a8226 Binary files /dev/null and b/notebook/static/base/images/favicon-busy-1.ico differ diff --git a/notebook/static/base/images/favicon-busy-2.ico b/notebook/static/base/images/favicon-busy-2.ico new file mode 100644 index 000000000..4a8b841c2 Binary files /dev/null and b/notebook/static/base/images/favicon-busy-2.ico differ diff --git a/notebook/static/base/images/favicon-busy-3.ico b/notebook/static/base/images/favicon-busy-3.ico new file mode 100644 index 000000000..b5edce573 Binary files /dev/null and b/notebook/static/base/images/favicon-busy-3.ico differ diff --git a/notebook/static/base/images/favicon-busy.ico b/notebook/static/base/images/favicon-busy.ico deleted file mode 100644 index 85f9995a4..000000000 Binary files a/notebook/static/base/images/favicon-busy.ico and /dev/null differ diff --git a/notebook/static/base/images/favicon-file.ico b/notebook/static/base/images/favicon-file.ico new file mode 100644 index 000000000..8167018cd Binary files /dev/null and b/notebook/static/base/images/favicon-file.ico differ diff --git a/notebook/static/base/images/favicon-notebook.ico b/notebook/static/base/images/favicon-notebook.ico new file mode 100644 index 000000000..4537e2d98 Binary files /dev/null and b/notebook/static/base/images/favicon-notebook.ico differ diff --git a/notebook/static/base/images/favicon-terminal.ico b/notebook/static/base/images/favicon-terminal.ico new file mode 100644 index 000000000..ace499a33 Binary files /dev/null and b/notebook/static/base/images/favicon-terminal.ico differ diff --git a/notebook/static/base/js/utils.js b/notebook/static/base/js/utils.js index b70b3ceb6..bc53a6c53 100644 --- a/notebook/static/base/js/utils.js +++ b/notebook/static/base/js/utils.js @@ -1017,6 +1017,36 @@ define([ } }; + + // javascript stores text as utf16 and string indices use "code units", + // which stores high-codepoint characters as "surrogate pairs", + // which occupy two indices in the javascript string. + // We need to translate cursor_pos in the protocol (in characters) + // to js offset (with surrogate pairs taking two spots). + function js_idx_to_char_idx (js_idx, text) { + var char_idx = js_idx; + for (var i = 0; i < text.length && i < js_idx; i++) { + var char_code = text.charCodeAt(i); + // check for the first half of a surrogate pair + if (char_code >= 0xD800 && char_code < 0xDC00) { + char_idx -= 1; + } + } + return char_idx; + } + + function char_idx_to_js_idx (char_idx, text) { + var js_idx = char_idx; + for (var i = 0; i < text.length && i < js_idx; i++) { + var char_code = text.charCodeAt(i); + // check for the first half of a surrogate pair + if (char_code >= 0xD800 && char_code < 0xDC00) { + js_idx += 1; + } + } + return js_idx; + } + // Test if a drag'n'drop event contains a file (as opposed to an HTML // element/text from the document) var dnd_contain_file = function(event) { @@ -1051,6 +1081,17 @@ define([ fn(); } } + + var change_favicon = function (src) { + var link = document.createElement('link'), + oldLink = document.getElementById('favicon'); + link.id = 'favicon'; + link.type = 'image/x-icon'; + link.rel = 'shortcut icon'; + link.href = utils.url_path_join(utils.get_body_data('baseUrl'), src); + if (oldLink) document.head.removeChild(oldLink); + document.head.appendChild(link); + }; var utils = { throttle: throttle, @@ -1101,7 +1142,10 @@ define([ format_datetime: format_datetime, datetime_sort_helper: datetime_sort_helper, dnd_contain_file: dnd_contain_file, - _ansispan:_ansispan + js_idx_to_char_idx: js_idx_to_char_idx, + char_idx_to_js_idx: char_idx_to_js_idx, + _ansispan:_ansispan, + change_favicon: change_favicon }; return utils; diff --git a/notebook/static/base/less/page.less b/notebook/static/base/less/page.less index 7a51af6a8..8bac02806 100644 --- a/notebook/static/base/less/page.less +++ b/notebook/static/base/less/page.less @@ -27,6 +27,10 @@ body > #header { z-index: 100; #header-container { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 5px; padding-bottom: 5px; padding-top: 5px; .border-box-sizing(); @@ -57,9 +61,6 @@ body > #header { padding-left: 0px; padding-top: (@navbar-height - @logo_height) / 2; padding-bottom: (@navbar-height - @logo_height) / 2; - @media (max-width: @screen-sm-max){ - margin-left: 10px; - } } @@ -99,8 +100,12 @@ input.ui-button { padding: 0.3em 0.9em; } +span#kernel_logo_widget { + margin: 0 10px; +} + span#login_widget { - float: right; + } span#login_widget > .button, diff --git a/notebook/static/edit/js/savewidget.js b/notebook/static/edit/js/savewidget.js index 31d7cc65d..7df4ed4d6 100644 --- a/notebook/static/edit/js/savewidget.js +++ b/notebook/static/edit/js/savewidget.js @@ -79,6 +79,11 @@ define([ class: "btn-primary", click: function () { var new_name = d.find('input').val(); + if (!new_name) { + // Reset the message + d.find('.rename-message').text("Enter a new filename:"); + return false; + } d.find('.rename-message').text("Renaming..."); d.find('input[type="text"]').prop('disabled', true); that.editor.rename(new_name).then( diff --git a/notebook/static/notebook/js/commandpalette.js b/notebook/static/notebook/js/commandpalette.js index 73a79b2d9..05de2e897 100644 --- a/notebook/static/notebook/js/commandpalette.js +++ b/notebook/static/notebook/js/commandpalette.js @@ -164,7 +164,11 @@ define(function(require){ // now src is the right structure for typeahead input.typeahead({ - emptyTemplate: "No results found for
{{query}}",
+ emptyTemplate: function(query) {
+ return $('').text(query)
+ );
+ },
maxItem: 1e3,
minLength: 0,
hint: true,
diff --git a/notebook/static/notebook/js/completer.js b/notebook/static/notebook/js/completer.js
index 8eb92ca2e..21a6a3b32 100644
--- a/notebook/static/notebook/js/completer.js
+++ b/notebook/static/notebook/js/completer.js
@@ -153,6 +153,8 @@ define([
// one kernel completion came back, finish_completing will be called with the results
// we fork here and directly call finish completing if kernel is busy
var cursor_pos = this.editor.indexFromPos(cur);
+ var text = this.editor.getValue();
+ cursor_pos = utils.js_idx_to_char_idx(cursor_pos, text);
if (this.skip_kernel_completion) {
this.finish_completing({ content: {
matches: [],
@@ -160,7 +162,7 @@ define([
cursor_end: cursor_pos,
}});
} else {
- this.cell.kernel.complete(this.editor.getValue(), cursor_pos,
+ this.cell.kernel.complete(text, cursor_pos,
$.proxy(this.finish_completing, this)
);
}
@@ -175,6 +177,7 @@ define([
var start = content.cursor_start;
var end = content.cursor_end;
var matches = content.matches;
+ console.log(content);
var cur = this.editor.getCursor();
if (end === null) {
@@ -187,7 +190,13 @@ define([
} else if (start < 0) {
start = end + start;
}
+ } else {
+ // handle surrogate pairs
+ var text = this.editor.getValue();
+ end = utils.char_idx_to_js_idx(end, text);
+ start = utils.char_idx_to_js_idx(start, text);
}
+
var results = CodeMirror.contextHint(this.editor);
var filtered_results = [];
//remove results from context completion
diff --git a/notebook/static/notebook/js/notificationarea.js b/notebook/static/notebook/js/notificationarea.js
index 121fc6be2..89abc33e0 100644
--- a/notebook/static/notebook/js/notificationarea.js
+++ b/notebook/static/notebook/js/notificationarea.js
@@ -40,6 +40,23 @@ define([
var $modal_ind_icon = $("#modal_indicator");
var $readonly_ind_icon = $('#readonly-indicator');
var $body = $('body');
+ var interval = 0;
+
+ var set_busy_favicon = function(on) {
+ if (on && !interval) {
+ var i = 0;
+ var icons = ['favicon-busy-1.ico', 'favicon-busy-3.ico', 'favicon-busy-3.ico'];
+ interval = setInterval(function() {
+ var icon = icons[i % 3];
+ utils.change_favicon('/static/base/images/' + icon);
+ i += 1;
+ }, 300);
+ } else {
+ clearInterval(interval);
+ utils.change_favicon('/static/base/images/favicon-notebook.ico');
+ interval = 0;
+ }
+ };
// Listen for the notebook loaded event. Set readonly indicator.
this.events.on('notebook_loaded.Notebook', function() {
@@ -244,41 +261,30 @@ define([
knw.danger(short, undefined, showMsg);
});
- var change_favicon = function (src) {
- var link = document.createElement('link'),
- oldLink = document.getElementById('favicon');
- link.id = 'favicon';
- link.type = 'image/x-icon';
- link.rel = 'shortcut icon';
- link.href = utils.url_path_join(utils.get_body_data('baseUrl'), src);
- if (oldLink) document.head.removeChild(oldLink);
- document.head.appendChild(link);
- };
-
this.events.on('kernel_starting.Kernel kernel_created.Session', function () {
// window.document.title='(Starting) '+window.document.title;
$kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
knw.set_message("Kernel starting, please wait...");
- change_favicon('/static/base/images/favicon-busy.ico');
+ set_busy_favicon(true);
});
this.events.on('kernel_ready.Kernel', function () {
// that.save_widget.update_document_title();
$kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
knw.info("Kernel ready", 500);
- change_favicon('/static/base/images/favicon.ico');
+ set_busy_favicon(false);
});
this.events.on('kernel_idle.Kernel', function () {
// that.save_widget.update_document_title();
$kernel_ind_icon.attr('class','kernel_idle_icon').attr('title','Kernel Idle');
- change_favicon('/static/base/images/favicon.ico');
+ set_busy_favicon(false);
});
this.events.on('kernel_busy.Kernel', function () {
// window.document.title='(Busy) '+window.document.title;
$kernel_ind_icon.attr('class','kernel_busy_icon').attr('title','Kernel Busy');
- change_favicon('/static/base/images/favicon-busy.ico');
+ set_busy_favicon(true);
});
this.events.on('spec_match_found.Kernel', function (evt, data) {
diff --git a/notebook/static/notebook/js/outputarea.js b/notebook/static/notebook/js/outputarea.js
index 71c7b5726..be1ffa89d 100644
--- a/notebook/static/notebook/js/outputarea.js
+++ b/notebook/static/notebook/js/outputarea.js
@@ -77,7 +77,7 @@ define([
this.prompt_overlay.addClass('out_prompt_overlay prompt');
this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
- this.collapse();
+ this.expand();
};
/**
@@ -976,6 +976,7 @@ define([
this._display_id_targets = {};
this.trusted = true;
this.unscroll_area();
+ this.expand();
return;
}
};
diff --git a/notebook/static/notebook/js/tooltip.js b/notebook/static/notebook/js/tooltip.js
index ba93a1d9c..c0d7e11e1 100644
--- a/notebook/static/notebook/js/tooltip.js
+++ b/notebook/static/notebook/js/tooltip.js
@@ -201,8 +201,8 @@ define([
this.cancel_pending();
var editor = cell.code_mirror;
var cursor = editor.getCursor();
- var cursor_pos = editor.indexFromPos(cursor);
var text = cell.get_text();
+ var cursor_pos = utils.js_idx_to_char_idx(editor.indexFromPos(cursor), text);
this._hide_if_no_docstring = hide_if_no_docstring;
diff --git a/notebook/static/notebook/less/kernelselector.less b/notebook/static/notebook/less/kernelselector.less
index 33c68a34c..fab2cf745 100644
--- a/notebook/static/notebook/less/kernelselector.less
+++ b/notebook/static/notebook/less/kernelselector.less
@@ -1,6 +1,4 @@
#kernel_logo_widget {
- .pull-right();
-
.current_kernel_logo {
display: none;
.navbar-vertical-align(32px);
diff --git a/notebook/static/notebook/less/savewidget.less b/notebook/static/notebook/less/savewidget.less
index fe37e1980..13f76b4d4 100644
--- a/notebook/static/notebook/less/savewidget.less
+++ b/notebook/static/notebook/less/savewidget.less
@@ -1,12 +1,15 @@
span.save_widget {
- margin-top: 6px;
- max-width: 100%;
+ height: 30px;
+ margin-top: 4px;
display: flex;
+ justify-content: flex-start;
+ align-items: baseline;
+ width: 50%;
+ flex: 1;
span.filename {
- height: 1em;
+ height: 100%;
line-height: 1em;
- padding: 3px;
margin-left: @padding-large-horizontal;
border: none;
font-size: 146.5%;
diff --git a/notebook/templates/edit.html b/notebook/templates/edit.html
index 9cda9fbc7..8c69952be 100644
--- a/notebook/templates/edit.html
+++ b/notebook/templates/edit.html
@@ -2,6 +2,8 @@
{% block title %}{{page_title}}{% endblock %}
+{% block favicon %}{% endblock %}
+
{% block stylesheet %}
diff --git a/notebook/templates/notebook.html b/notebook/templates/notebook.html
index 53e5ebbb4..faa82cd87 100644
--- a/notebook/templates/notebook.html
+++ b/notebook/templates/notebook.html
@@ -1,5 +1,7 @@
{% extends "page.html" %}
+{% block favicon %}{% endblock %}
+
{% block stylesheet %}
{% if mathjax_url %}
@@ -36,7 +38,7 @@ data-notebook-path="{{notebook_path | urlencode}}"
{% block headercontainer %}
-