From 4e4c27a343fd4da8f3cc8bb573ba34a85c30b545 2011-10-26 05:14:09 From: MinRK Date: 2011-10-26 05:14:09 Subject: [PATCH] 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. --- diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 97b2937..ceb5227 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 27db5ad..466d165 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 8d2631c..ba113b3 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 a25d2e2..2d86c62 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 b557e29..044016e 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 e8c0f84..b55d7fb 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 5a3bf86..06156d8 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 3039ad5..40e69bc 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 0b9075f..fb87e60 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 @@ +