From 9a7fda926df1088c8a985096ef87e9f45f57539c Mon Sep 17 00:00:00 2001 From: MinRK Date: Mon, 24 Oct 2011 21:33:05 -0700 Subject: [PATCH 1/5] Allow notebook server to run in read-only mode Kernels are never started, and all save/delete/execution handlers raise 403: Forbidden. /cc @fperez --- IPython/frontend/html/notebook/handlers.py | 20 +++++++++++++++++++ IPython/frontend/html/notebook/notebookapp.py | 10 +++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 1ce17b589..97b293766 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -26,6 +26,7 @@ from tornado import websocket from zmq.eventloop import ioloop from zmq.utils import jsonapi +from IPython.external.decorator import decorator from IPython.zmq.session import Session try: @@ -34,6 +35,16 @@ except ImportError: publish_string = None +#----------------------------------------------------------------------------- +# Decorator for disabling read-only handlers +#----------------------------------------------------------------------------- + +@decorator +def not_if_readonly(f, self, *args, **kwargs): + if self.application.ipython_app.read_only: + raise web.HTTPError(403, "Notebook server is read-only") + else: + return f(self, *args, **kwargs) #----------------------------------------------------------------------------- # Top-level handlers @@ -82,6 +93,7 @@ class LoginHandler(AuthenticatedHandler): class NewHandler(AuthenticatedHandler): + @not_if_readonly @web.authenticated def get(self): nbm = self.application.notebook_manager @@ -118,11 +130,13 @@ class NamedNotebookHandler(AuthenticatedHandler): class MainKernelHandler(AuthenticatedHandler): + @not_if_readonly @web.authenticated def get(self): km = self.application.kernel_manager self.finish(jsonapi.dumps(km.kernel_ids)) + @not_if_readonly @web.authenticated def post(self): km = self.application.kernel_manager @@ -138,6 +152,7 @@ class KernelHandler(AuthenticatedHandler): SUPPORTED_METHODS = ('DELETE') + @not_if_readonly @web.authenticated def delete(self, kernel_id): km = self.application.kernel_manager @@ -148,6 +163,7 @@ class KernelHandler(AuthenticatedHandler): class KernelActionHandler(AuthenticatedHandler): + @not_if_readonly @web.authenticated def post(self, kernel_id, action): km = self.application.kernel_manager @@ -226,6 +242,7 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler): except: logging.warn("couldn't parse cookie string: %s",msg, exc_info=True) + @not_if_readonly def on_first_message(self, msg): self._inject_cookie_message(msg) if self.get_current_user() is None: @@ -369,6 +386,7 @@ class NotebookRootHandler(AuthenticatedHandler): files = nbm.list_notebooks() self.finish(jsonapi.dumps(files)) + @not_if_readonly @web.authenticated def post(self): nbm = self.application.notebook_manager @@ -401,6 +419,7 @@ class NotebookHandler(AuthenticatedHandler): self.set_header('Last-Modified', last_mod) self.finish(data) + @not_if_readonly @web.authenticated def put(self, notebook_id): nbm = self.application.notebook_manager @@ -410,6 +429,7 @@ class NotebookHandler(AuthenticatedHandler): self.set_status(204) self.finish() + @not_if_readonly @web.authenticated def delete(self, notebook_id): nbm = self.application.notebook_manager diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index d037f95dd..27db5ad49 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -116,6 +116,10 @@ flags['no-browser']=( {'NotebookApp' : {'open_browser' : False}}, "Don't open the notebook in a browser after startup." ) +flags['read-only'] = ( + {'NotebookApp' : {'read_only' : True}}, + "Launch the Notebook server in read-only mode, not allowing execution or editing" +) # the flags that are specific to the frontend # these must be scrubbed before being passed to the kernel, @@ -203,6 +207,10 @@ class NotebookApp(BaseIPythonApplication): open_browser = Bool(True, config=True, help="Whether to open in a browser after starting.") + + read_only = Bool(False, config=True, + help="Whether to prevent editing/execution of notebooks." + ) def get_ws_url(self): """Return the WebSocket URL for this server.""" @@ -282,7 +290,7 @@ class NotebookApp(BaseIPythonApplication): # Try random ports centered around the default. from random import randint n = 50 # Max number of attempts, keep reasonably large. - for port in [self.port] + [self.port + randint(-2*n, 2*n) for i in range(n)]: + for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]: try: self.http_server.listen(port, self.ip) except socket.error, e: From a6de5947decf788d394826f0795e6d2c4712b92b Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 25 Oct 2011 22:14:09 -0700 Subject: [PATCH 2/5] add read-only view for notebooks When using a password, read-only mode allows unauthenticated users read-only access to notebooks. Editing, execution, etc. are not allowed in read-only mode, but save/print functions are available. No kernels are started until an authenticated user opens a notebook. --- IPython/frontend/html/notebook/handlers.py | 49 +++++++++++++------ IPython/frontend/html/notebook/notebookapp.py | 13 ++++- .../html/notebook/static/css/base.css | 9 ++++ .../frontend/html/notebook/static/js/cell.js | 4 ++ .../html/notebook/static/js/codecell.js | 1 + .../html/notebook/static/js/notebook.js | 30 ++++++++++-- .../html/notebook/static/js/notebooklist.js | 14 +++++- .../html/notebook/static/js/notebookmain.js | 1 + .../static/js/projectdashboardmain.js | 1 + .../html/notebook/static/js/textcell.js | 4 +- .../html/notebook/templates/notebook.html | 6 ++- .../notebook/templates/projectdashboard.html | 4 ++ 12 files changed, 114 insertions(+), 22 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 97b293766..ceb522786 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -46,6 +46,22 @@ def not_if_readonly(f, self, *args, **kwargs): else: return f(self, *args, **kwargs) +@decorator +def authenticate_unless_readonly(f, self, *args, **kwargs): + """authenticate this page *unless* readonly view is active. + + In read-only mode, the notebook list and print view should + be accessible without authentication. + """ + + @web.authenticated + def auth_f(self, *args, **kwargs): + return f(self, *args, **kwargs) + if self.application.ipython_app.read_only: + return f(self, *args, **kwargs) + else: + return auth_f(self, *args, **kwargs) + #----------------------------------------------------------------------------- # Top-level handlers #----------------------------------------------------------------------------- @@ -68,7 +84,7 @@ class AuthenticatedHandler(web.RequestHandler): class ProjectDashboardHandler(AuthenticatedHandler): - @web.authenticated + @authenticate_unless_readonly def get(self): nbm = self.application.notebook_manager project = nbm.notebook_dir @@ -81,7 +97,7 @@ class ProjectDashboardHandler(AuthenticatedHandler): class LoginHandler(AuthenticatedHandler): def get(self): - self.render('login.html', next='/') + self.render('login.html', next=self.get_argument('next', default='/')) def post(self): pwd = self.get_argument('password', default=u'') @@ -93,7 +109,6 @@ class LoginHandler(AuthenticatedHandler): class NewHandler(AuthenticatedHandler): - @not_if_readonly @web.authenticated def get(self): nbm = self.application.notebook_manager @@ -109,7 +124,7 @@ class NewHandler(AuthenticatedHandler): class NamedNotebookHandler(AuthenticatedHandler): - @web.authenticated + @authenticate_unless_readonly def get(self, notebook_id): nbm = self.application.notebook_manager project = nbm.notebook_dir @@ -130,13 +145,11 @@ class NamedNotebookHandler(AuthenticatedHandler): class MainKernelHandler(AuthenticatedHandler): - @not_if_readonly @web.authenticated def get(self): km = self.application.kernel_manager self.finish(jsonapi.dumps(km.kernel_ids)) - @not_if_readonly @web.authenticated def post(self): km = self.application.kernel_manager @@ -152,7 +165,6 @@ class KernelHandler(AuthenticatedHandler): SUPPORTED_METHODS = ('DELETE') - @not_if_readonly @web.authenticated def delete(self, kernel_id): km = self.application.kernel_manager @@ -163,7 +175,6 @@ class KernelHandler(AuthenticatedHandler): class KernelActionHandler(AuthenticatedHandler): - @not_if_readonly @web.authenticated def post(self, kernel_id, action): km = self.application.kernel_manager @@ -242,7 +253,6 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler): except: logging.warn("couldn't parse cookie string: %s",msg, exc_info=True) - @not_if_readonly def on_first_message(self, msg): self._inject_cookie_message(msg) if self.get_current_user() is None: @@ -380,13 +390,19 @@ class ShellHandler(AuthenticatedZMQStreamHandler): class NotebookRootHandler(AuthenticatedHandler): - @web.authenticated + @authenticate_unless_readonly def get(self): + + # communicate read-only via Allow header + if self.application.ipython_app.read_only and not self.get_current_user(): + self.set_header('Allow', 'GET') + else: + self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS)) + nbm = self.application.notebook_manager files = nbm.list_notebooks() self.finish(jsonapi.dumps(files)) - @not_if_readonly @web.authenticated def post(self): nbm = self.application.notebook_manager @@ -405,11 +421,18 @@ class NotebookHandler(AuthenticatedHandler): SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE') - @web.authenticated + @authenticate_unless_readonly def get(self, notebook_id): nbm = self.application.notebook_manager format = self.get_argument('format', default='json') last_mod, name, data = nbm.get_notebook(notebook_id, format) + + # communicate read-only via Allow header + if self.application.ipython_app.read_only and not self.get_current_user(): + self.set_header('Allow', 'GET') + else: + self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS)) + if format == u'json': self.set_header('Content-Type', 'application/json') self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name) @@ -419,7 +442,6 @@ class NotebookHandler(AuthenticatedHandler): self.set_header('Last-Modified', last_mod) self.finish(data) - @not_if_readonly @web.authenticated def put(self, notebook_id): nbm = self.application.notebook_manager @@ -429,7 +451,6 @@ class NotebookHandler(AuthenticatedHandler): self.set_status(204) self.finish() - @not_if_readonly @web.authenticated def delete(self, notebook_id): nbm = self.application.notebook_manager diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 27db5ad49..466d1656a 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -118,13 +118,22 @@ flags['no-browser']=( ) flags['read-only'] = ( {'NotebookApp' : {'read_only' : True}}, - "Launch the Notebook server in read-only mode, not allowing execution or editing" + """Allow read-only access to notebooks. + + When using a password to protect the notebook server, this flag + allows unauthenticated clients to view the notebook list, and + individual notebooks, but not edit them, start kernels, or run + code. + + This flag only makes sense in conjunction with setting a password, + via the ``NotebookApp.password`` configurable. + """ ) # the flags that are specific to the frontend # these must be scrubbed before being passed to the kernel, # or it will raise an error on unrecognized flags -notebook_flags = ['no-browser'] +notebook_flags = ['no-browser', 'read-only'] aliases = dict(ipkernel_aliases) diff --git a/IPython/frontend/html/notebook/static/css/base.css b/IPython/frontend/html/notebook/static/css/base.css index 8d2631c63..ba113b39d 100644 --- a/IPython/frontend/html/notebook/static/css/base.css +++ b/IPython/frontend/html/notebook/static/css/base.css @@ -51,3 +51,12 @@ div#main_app { padding: 0.2em 0.8em; font-size: 77%; } + +span#login_widget { + float: right; +} + +/* generic class for hidden objects */ +.hidden { + display: none; +} \ No newline at end of file diff --git a/IPython/frontend/html/notebook/static/js/cell.js b/IPython/frontend/html/notebook/static/js/cell.js index a25d2e2e0..2d86c6265 100644 --- a/IPython/frontend/html/notebook/static/js/cell.js +++ b/IPython/frontend/html/notebook/static/js/cell.js @@ -15,6 +15,10 @@ var IPython = (function (IPython) { var Cell = function (notebook) { this.notebook = notebook; + this.read_only = false; + if (notebook){ + this.read_only = notebook.read_only; + } this.selected = false; this.element = null; this.create_element(); diff --git a/IPython/frontend/html/notebook/static/js/codecell.js b/IPython/frontend/html/notebook/static/js/codecell.js index b557e29ab..044016e07 100644 --- a/IPython/frontend/html/notebook/static/js/codecell.js +++ b/IPython/frontend/html/notebook/static/js/codecell.js @@ -37,6 +37,7 @@ var IPython = (function (IPython) { indentUnit : 4, mode: 'python', theme: 'ipython', + readOnly: this.read_only, onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this) }); input.append(input_area); diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js index e8c0f84ff..b55d7fbdb 100644 --- a/IPython/frontend/html/notebook/static/js/notebook.js +++ b/IPython/frontend/html/notebook/static/js/notebook.js @@ -14,6 +14,7 @@ var IPython = (function (IPython) { var utils = IPython.utils; var Notebook = function (selector) { + this.read_only = false; this.element = $(selector); this.element.scroll(); this.element.data("notebook", this); @@ -42,6 +43,7 @@ var IPython = (function (IPython) { var that = this; var end_space = $('
').height(150); end_space.dblclick(function (e) { + if (that.read_only) return; var ncells = that.ncells(); that.insert_code_cell_below(ncells-1); }); @@ -54,6 +56,7 @@ var IPython = (function (IPython) { var that = this; $(document).keydown(function (event) { // console.log(event); + if (that.read_only) return; if (event.which === 38) { var cell = that.selected_cell(); if (cell.at_top()) { @@ -185,11 +188,11 @@ var IPython = (function (IPython) { }); $(window).bind('beforeunload', function () { - var kill_kernel = $('#kill_kernel').prop('checked'); + var kill_kernel = $('#kill_kernel').prop('checked'); if (kill_kernel) { that.kernel.kill(); } - if (that.dirty) { + if (that.dirty && ! that.read_only) { return "You have unsaved changes that will be lost if you leave this page."; }; }); @@ -975,14 +978,26 @@ var IPython = (function (IPython) { Notebook.prototype.notebook_loaded = function (data, status, xhr) { + var allowed = xhr.getResponseHeader('Allow'); + if (allowed && allowed.indexOf('PUT') == -1){ + this.read_only = true; + // unhide login button if it's relevant + $('span#login_widget').removeClass('hidden'); + }else{ + this.read_only = false; + } this.fromJSON(data); if (this.ncells() === 0) { this.insert_code_cell_below(); }; IPython.save_widget.status_save(); IPython.save_widget.set_notebook_name(data.metadata.name); - this.start_kernel(); this.dirty = false; + if (this.read_only) { + this.handle_read_only(); + }else{ + this.start_kernel(); + } // fromJSON always selects the last cell inserted. We need to wait // until that is done before scrolling to the top. setTimeout(function () { @@ -992,6 +1007,15 @@ var IPython = (function (IPython) { }; + Notebook.prototype.handle_read_only = function(){ + IPython.left_panel.collapse(); + IPython.save_widget.element.find('button#save_notebook').addClass('hidden'); + $('button#new_notebook').addClass('hidden'); + $('div#cell_section').addClass('hidden'); + $('div#kernel_section').addClass('hidden'); + } + + IPython.Notebook = Notebook; diff --git a/IPython/frontend/html/notebook/static/js/notebooklist.js b/IPython/frontend/html/notebook/static/js/notebooklist.js index 5a3bf8627..06156d879 100644 --- a/IPython/frontend/html/notebook/static/js/notebooklist.js +++ b/IPython/frontend/html/notebook/static/js/notebooklist.js @@ -73,6 +73,15 @@ var IPython = (function (IPython) { NotebookList.prototype.list_loaded = function (data, status, xhr) { + var allowed = xhr.getResponseHeader('Allow'); + if (allowed && allowed.indexOf('PUT') == -1){ + this.read_only = true; + $('#new_notebook').addClass('hidden'); + // unhide login button if it's relevant + $('span#login_widget').removeClass('hidden'); + }else{ + this.read_only = false; + } var len = data.length; // Todo: remove old children for (var i=0; i').addClass('text_cell_render'). @@ -65,6 +66,7 @@ var IPython = (function (IPython) { TextCell.prototype.edit = function () { + if ( this.read_only ) return; if (this.rendered === true) { var text_cell = this.element; var output = text_cell.find("div.text_cell_render"); diff --git a/IPython/frontend/html/notebook/templates/notebook.html b/IPython/frontend/html/notebook/templates/notebook.html index 3039ad54c..40e69bcc0 100644 --- a/IPython/frontend/html/notebook/templates/notebook.html +++ b/IPython/frontend/html/notebook/templates/notebook.html @@ -57,7 +57,10 @@ - + + Idle @@ -278,6 +281,7 @@ + diff --git a/IPython/frontend/html/notebook/templates/projectdashboard.html b/IPython/frontend/html/notebook/templates/projectdashboard.html index 0b9075fc4..fb87e60b7 100644 --- a/IPython/frontend/html/notebook/templates/projectdashboard.html +++ b/IPython/frontend/html/notebook/templates/projectdashboard.html @@ -19,6 +19,9 @@
@@ -54,6 +57,7 @@ + From a18ddb9ec41a2e5640ead0cb12e514db9e7a5a51 Mon Sep 17 00:00:00 2001 From: MinRK Date: Thu, 27 Oct 2011 21:55:21 -0700 Subject: [PATCH 3/5] add missing loginwidget.js --- .../html/notebook/static/js/loginwidget.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 IPython/frontend/html/notebook/static/js/loginwidget.js diff --git a/IPython/frontend/html/notebook/static/js/loginwidget.js b/IPython/frontend/html/notebook/static/js/loginwidget.js new file mode 100644 index 000000000..17fcc5883 --- /dev/null +++ b/IPython/frontend/html/notebook/static/js/loginwidget.js @@ -0,0 +1,38 @@ +//---------------------------------------------------------------------------- +// Copyright (C) 2008-2011 The IPython Development Team +// +// Distributed under the terms of the BSD License. The full license is in +// the file COPYING, distributed as part of this software. +//---------------------------------------------------------------------------- + +//============================================================================ +// Login button +//============================================================================ + +var IPython = (function (IPython) { + + var LoginWidget = function (selector) { + this.selector = selector; + if (this.selector !== undefined) { + this.element = $(selector); + this.style(); + this.bind_events(); + } + }; + + LoginWidget.prototype.style = function () { + this.element.find('button#login').button(); + }; + LoginWidget.prototype.bind_events = function () { + var that = this; + this.element.find("button#login").click(function () { + window.location = "/login?next="+location.pathname; + }); + }; + + // Set module variables + IPython.LoginWidget = LoginWidget; + + return IPython; + +}(IPython)); From a3a0be08bba6b59787498b092300d9d446e64cc3 Mon Sep 17 00:00:00 2001 From: MinRK Date: Thu, 27 Oct 2011 23:23:34 -0700 Subject: [PATCH 4/5] allow fully read-only mode if no password is set --- IPython/frontend/html/notebook/handlers.py | 2 +- IPython/frontend/html/notebook/notebookapp.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index ceb522786..ff903d5ad 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -77,7 +77,7 @@ class AuthenticatedHandler(web.RequestHandler): if user_id is None: # prevent extra Invalid cookie sig warnings: self.clear_cookie('username') - if not self.application.password: + if not self.application.password and not self.application.ipython_app.read_only: user_id = 'anonymous' return user_id diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 466d1656a..71a53f9ea 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -125,8 +125,7 @@ flags['read-only'] = ( individual notebooks, but not edit them, start kernels, or run code. - This flag only makes sense in conjunction with setting a password, - via the ``NotebookApp.password`` configurable. + If no password is set, the server will be entirely read-only. """ ) From 65a6cb3b1c5d5a85000170f758ac5012b7bba56f Mon Sep 17 00:00:00 2001 From: MinRK Date: Fri, 28 Oct 2011 14:45:41 -0700 Subject: [PATCH 5/5] move read_only flag to page-level contents of LPanel are not drawn until after collapse read_only is in a tag --- IPython/frontend/html/notebook/handlers.py | 47 ++++++++++--------- IPython/frontend/html/notebook/notebookapp.py | 1 + .../html/notebook/static/js/notebook.js | 23 +-------- .../html/notebook/static/js/notebooklist.js | 11 +---- .../html/notebook/static/js/notebookmain.js | 24 ++++++++++ .../static/js/projectdashboardmain.js | 8 ++++ .../html/notebook/templates/login.html | 2 + .../html/notebook/templates/notebook.html | 3 +- .../notebook/templates/projectdashboard.html | 2 + 9 files changed, 68 insertions(+), 53 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index ff903d5ad..8e543e3bd 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -41,7 +41,7 @@ except ImportError: @decorator def not_if_readonly(f, self, *args, **kwargs): - if self.application.ipython_app.read_only: + if self.application.read_only: raise web.HTTPError(403, "Notebook server is read-only") else: return f(self, *args, **kwargs) @@ -57,7 +57,7 @@ def authenticate_unless_readonly(f, self, *args, **kwargs): @web.authenticated def auth_f(self, *args, **kwargs): return f(self, *args, **kwargs) - if self.application.ipython_app.read_only: + if self.application.read_only: return f(self, *args, **kwargs) else: return auth_f(self, *args, **kwargs) @@ -77,9 +77,20 @@ class AuthenticatedHandler(web.RequestHandler): if user_id is None: # prevent extra Invalid cookie sig warnings: self.clear_cookie('username') - if not self.application.password and not self.application.ipython_app.read_only: + if not self.application.password and not self.application.read_only: user_id = 'anonymous' return user_id + + @property + def read_only(self): + if self.application.read_only: + if self.application.password: + return self.get_current_user() is None + else: + return True + else: + return False + class ProjectDashboardHandler(AuthenticatedHandler): @@ -90,21 +101,24 @@ class ProjectDashboardHandler(AuthenticatedHandler): project = nbm.notebook_dir self.render( 'projectdashboard.html', project=project, - base_project_url=u'/', base_kernel_url=u'/' + base_project_url=u'/', base_kernel_url=u'/', + read_only=self.read_only, ) class LoginHandler(AuthenticatedHandler): def get(self): - self.render('login.html', next=self.get_argument('next', default='/')) + self.render('login.html', + next=self.get_argument('next', default='/'), + read_only=self.read_only, + ) def post(self): pwd = self.get_argument('password', default=u'') if self.application.password and pwd == self.application.password: self.set_secure_cookie('username', str(uuid.uuid4())) - url = self.get_argument('next', default='/') - self.redirect(url) + self.redirect(self.get_argument('next', default='/')) class NewHandler(AuthenticatedHandler): @@ -118,7 +132,8 @@ class NewHandler(AuthenticatedHandler): 'notebook.html', project=project, notebook_id=notebook_id, base_project_url=u'/', base_kernel_url=u'/', - kill_kernel=False + kill_kernel=False, + read_only=False, ) @@ -130,11 +145,13 @@ class NamedNotebookHandler(AuthenticatedHandler): project = nbm.notebook_dir if not nbm.notebook_exists(notebook_id): raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) + self.render( 'notebook.html', project=project, notebook_id=notebook_id, base_project_url=u'/', base_kernel_url=u'/', - kill_kernel=False + kill_kernel=False, + read_only=self.read_only, ) @@ -393,12 +410,6 @@ class NotebookRootHandler(AuthenticatedHandler): @authenticate_unless_readonly def get(self): - # communicate read-only via Allow header - if self.application.ipython_app.read_only and not self.get_current_user(): - self.set_header('Allow', 'GET') - else: - self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS)) - nbm = self.application.notebook_manager files = nbm.list_notebooks() self.finish(jsonapi.dumps(files)) @@ -427,12 +438,6 @@ class NotebookHandler(AuthenticatedHandler): format = self.get_argument('format', default='json') last_mod, name, data = nbm.get_notebook(notebook_id, format) - # communicate read-only via Allow header - if self.application.ipython_app.read_only and not self.get_current_user(): - self.set_header('Allow', 'GET') - else: - self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS)) - if format == u'json': self.set_header('Content-Type', 'application/json') self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name) diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 71a53f9ea..8ae10840a 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -105,6 +105,7 @@ class NotebookWebApplication(web.Application): self.log = log self.notebook_manager = notebook_manager self.ipython_app = ipython_app + self.read_only = self.ipython_app.read_only #----------------------------------------------------------------------------- diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js index b55d7fbdb..c27f701cd 100644 --- a/IPython/frontend/html/notebook/static/js/notebook.js +++ b/IPython/frontend/html/notebook/static/js/notebook.js @@ -14,7 +14,7 @@ var IPython = (function (IPython) { var utils = IPython.utils; var Notebook = function (selector) { - this.read_only = false; + this.read_only = IPython.read_only; this.element = $(selector); this.element.scroll(); this.element.data("notebook", this); @@ -979,13 +979,6 @@ var IPython = (function (IPython) { Notebook.prototype.notebook_loaded = function (data, status, xhr) { var allowed = xhr.getResponseHeader('Allow'); - if (allowed && allowed.indexOf('PUT') == -1){ - this.read_only = true; - // unhide login button if it's relevant - $('span#login_widget').removeClass('hidden'); - }else{ - this.read_only = false; - } this.fromJSON(data); if (this.ncells() === 0) { this.insert_code_cell_below(); @@ -993,9 +986,7 @@ var IPython = (function (IPython) { IPython.save_widget.status_save(); IPython.save_widget.set_notebook_name(data.metadata.name); this.dirty = false; - if (this.read_only) { - this.handle_read_only(); - }else{ + if (! this.read_only) { this.start_kernel(); } // fromJSON always selects the last cell inserted. We need to wait @@ -1006,16 +997,6 @@ var IPython = (function (IPython) { }, 50); }; - - Notebook.prototype.handle_read_only = function(){ - IPython.left_panel.collapse(); - IPython.save_widget.element.find('button#save_notebook').addClass('hidden'); - $('button#new_notebook').addClass('hidden'); - $('div#cell_section').addClass('hidden'); - $('div#kernel_section').addClass('hidden'); - } - - IPython.Notebook = Notebook; diff --git a/IPython/frontend/html/notebook/static/js/notebooklist.js b/IPython/frontend/html/notebook/static/js/notebooklist.js index 06156d879..316ef0e38 100644 --- a/IPython/frontend/html/notebook/static/js/notebooklist.js +++ b/IPython/frontend/html/notebook/static/js/notebooklist.js @@ -73,15 +73,6 @@ var IPython = (function (IPython) { NotebookList.prototype.list_loaded = function (data, status, xhr) { - var allowed = xhr.getResponseHeader('Allow'); - if (allowed && allowed.indexOf('PUT') == -1){ - this.read_only = true; - $('#new_notebook').addClass('hidden'); - // unhide login button if it's relevant - $('span#login_widget').removeClass('hidden'); - }else{ - this.read_only = false; - } var len = data.length; // Todo: remove old children for (var i=0; i + + diff --git a/IPython/frontend/html/notebook/templates/notebook.html b/IPython/frontend/html/notebook/templates/notebook.html index 40e69bcc0..0d924fc46 100644 --- a/IPython/frontend/html/notebook/templates/notebook.html +++ b/IPython/frontend/html/notebook/templates/notebook.html @@ -40,7 +40,8 @@ - + + diff --git a/IPython/frontend/html/notebook/templates/projectdashboard.html b/IPython/frontend/html/notebook/templates/projectdashboard.html index fb87e60b7..386e2613a 100644 --- a/IPython/frontend/html/notebook/templates/projectdashboard.html +++ b/IPython/frontend/html/notebook/templates/projectdashboard.html @@ -12,6 +12,8 @@ + +