diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 1ce17b5..8e543e3 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,32 @@ except ImportError: publish_string = None +#----------------------------------------------------------------------------- +# Decorator for disabling read-only handlers +#----------------------------------------------------------------------------- + +@decorator +def not_if_readonly(f, self, *args, **kwargs): + if self.application.read_only: + raise web.HTTPError(403, "Notebook server is read-only") + 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.read_only: + return f(self, *args, **kwargs) + else: + return auth_f(self, *args, **kwargs) #----------------------------------------------------------------------------- # Top-level handlers @@ -50,34 +77,48 @@ 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.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): - @web.authenticated + @authenticate_unless_readonly def get(self): nbm = self.application.notebook_manager 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.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): @@ -91,23 +132,26 @@ 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, ) class NamedNotebookHandler(AuthenticatedHandler): - @web.authenticated + @authenticate_unless_readonly def get(self, notebook_id): nbm = self.application.notebook_manager 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, ) @@ -363,8 +407,9 @@ class ShellHandler(AuthenticatedZMQStreamHandler): class NotebookRootHandler(AuthenticatedHandler): - @web.authenticated + @authenticate_unless_readonly def get(self): + nbm = self.application.notebook_manager files = nbm.list_notebooks() self.finish(jsonapi.dumps(files)) @@ -387,11 +432,12 @@ 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) + 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 ae9fb18..2f0f50f 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -110,6 +110,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 #----------------------------------------------------------------------------- @@ -121,11 +122,23 @@ flags['no-browser']=( {'NotebookApp' : {'open_browser' : False}}, "Don't open the notebook in a browser after startup." ) +flags['read-only'] = ( + {'NotebookApp' : {'read_only' : True}}, + """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. + + If no password is set, the server will be entirely read-only. + """ +) # 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) @@ -208,6 +221,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.""" @@ -288,7 +305,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: 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/leftpanel.js b/IPython/frontend/html/notebook/static/js/leftpanel.js index 4a41294..380510a 100644 --- a/IPython/frontend/html/notebook/static/js/leftpanel.js +++ b/IPython/frontend/html/notebook/static/js/leftpanel.js @@ -65,8 +65,10 @@ var IPython = (function (IPython) { LeftPanel.prototype.create_children = function () { this.notebook_section = new IPython.NotebookSection('div#notebook_section'); - this.cell_section = new IPython.CellSection('div#cell_section'); - this.kernel_section = new IPython.KernelSection('div#kernel_section'); + if (! IPython.read_only){ + this.cell_section = new IPython.CellSection('div#cell_section'); + this.kernel_section = new IPython.KernelSection('div#kernel_section'); + } this.help_section = new IPython.HelpSection('div#help_section'); } 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 0000000..17fcc58 --- /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)); diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js index e8c0f84..c27f701 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 = IPython.read_only; 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,17 @@ var IPython = (function (IPython) { Notebook.prototype.notebook_loaded = function (data, status, xhr) { + var allowed = xhr.getResponseHeader('Allow'); 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.start_kernel(); + } // fromJSON always selects the last cell inserted. We need to wait // until that is done before scrolling to the top. setTimeout(function () { @@ -991,7 +997,6 @@ var IPython = (function (IPython) { }, 50); }; - 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..316ef0e 100644 --- a/IPython/frontend/html/notebook/static/js/notebooklist.js +++ b/IPython/frontend/html/notebook/static/js/notebooklist.js @@ -80,7 +80,10 @@ var IPython = (function (IPython) { var nbname = data[i].name; var item = this.new_notebook_item(i); this.add_link(notebook_id, nbname, item); - this.add_delete_button(item); + if (!IPython.read_only){ + // hide delete buttons when readonly + this.add_delete_button(item); + } }; }; diff --git a/IPython/frontend/html/notebook/static/js/notebookmain.js b/IPython/frontend/html/notebook/static/js/notebookmain.js index 095d098..11c0db6 100644 --- a/IPython/frontend/html/notebook/static/js/notebookmain.js +++ b/IPython/frontend/html/notebook/static/js/notebookmain.js @@ -23,6 +23,7 @@ $(document).ready(function () { } }); IPython.markdown_converter = new Markdown.Converter(); + IPython.read_only = $('meta[name=read_only]').attr("content") == 'True'; $('div#header').addClass('border-box-sizing'); $('div#main_app').addClass('border-box-sizing ui-widget ui-widget-content'); @@ -33,6 +34,7 @@ $(document).ready(function () { IPython.left_panel = new IPython.LeftPanel('div#left_panel', 'div#left_panel_splitter'); IPython.save_widget = new IPython.SaveWidget('span#save_widget'); IPython.quick_help = new IPython.QuickHelp('span#quick_help_area'); + IPython.login_widget = new IPython.LoginWidget('span#login_widget'); IPython.print_widget = new IPython.PrintWidget('span#print_widget'); IPython.notebook = new IPython.Notebook('div#notebook'); IPython.kernel_status_widget = new IPython.KernelStatusWidget('#kernel_status'); @@ -42,6 +44,21 @@ $(document).ready(function () { // These have display: none in the css file and are made visible here to prevent FLOUC. $('div#header').css('display','block'); + + if(IPython.read_only){ + // hide various elements from read-only view + IPython.save_widget.element.find('button#save_notebook').addClass('hidden'); + IPython.quick_help.element.addClass('hidden'); // shortcuts are disabled in read_only + $('button#new_notebook').addClass('hidden'); + $('div#cell_section').addClass('hidden'); + $('div#kernel_section').addClass('hidden'); + $('span#login_widget').removeClass('hidden'); + // left panel starts collapsed, but the collapse must happen after + // elements start drawing. Don't draw contents of the panel until + // after they are collapsed + IPython.left_panel.left_panel_element.css('visibility', 'hidden'); + } + $('div#main_app').css('display','block'); // Perform these actions after the notebook has been loaded. @@ -52,6 +69,14 @@ $(document).ready(function () { IPython.save_widget.update_url(); IPython.layout_manager.do_resize(); IPython.pager.collapse(); + if(IPython.read_only){ + // collapse the left panel on read-only + IPython.left_panel.collapse(); + // and finally unhide the panel contents after collapse + setTimeout(function(){ + IPython.left_panel.left_panel_element.css('visibility', 'visible'); + }, 200) + } },100); }); diff --git a/IPython/frontend/html/notebook/static/js/projectdashboardmain.js b/IPython/frontend/html/notebook/static/js/projectdashboardmain.js index 9ae6256..4d6f9d3 100644 --- a/IPython/frontend/html/notebook/static/js/projectdashboardmain.js +++ b/IPython/frontend/html/notebook/static/js/projectdashboardmain.js @@ -27,7 +27,16 @@ $(document).ready(function () { $('div#left_panel').addClass('box-flex'); $('div#right_panel').addClass('box-flex'); + IPython.read_only = $('meta[name=read_only]').attr("content") == 'True'; + IPython.notebook_list = new IPython.NotebookList('div#notebook_list'); + IPython.login_widget = new IPython.LoginWidget('span#login_widget'); + + if (IPython.read_only){ + $('#new_notebook').addClass('hidden'); + // unhide login button if it's relevant + $('span#login_widget').removeClass('hidden'); + } IPython.notebook_list.load_list(); // These have display: none in the css file and are made visible here to prevent FLOUC. diff --git a/IPython/frontend/html/notebook/static/js/textcell.js b/IPython/frontend/html/notebook/static/js/textcell.js index 4a24e53..817c0dc 100644 --- a/IPython/frontend/html/notebook/static/js/textcell.js +++ b/IPython/frontend/html/notebook/static/js/textcell.js @@ -33,7 +33,8 @@ var IPython = (function (IPython) { indentUnit : 4, mode: this.code_mirror_mode, theme: 'default', - value: this.placeholder + value: this.placeholder, + readOnly: this.read_only, }); // The tabindex=-1 makes this div focusable. var render_area = $('').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/login.html b/IPython/frontend/html/notebook/templates/login.html index 86f160a..03b7b1c 100644 --- a/IPython/frontend/html/notebook/templates/login.html +++ b/IPython/frontend/html/notebook/templates/login.html @@ -11,6 +11,8 @@ + + diff --git a/IPython/frontend/html/notebook/templates/notebook.html b/IPython/frontend/html/notebook/templates/notebook.html index 3039ad5..0d924fc 100644 --- a/IPython/frontend/html/notebook/templates/notebook.html +++ b/IPython/frontend/html/notebook/templates/notebook.html @@ -40,7 +40,8 @@ - + + @@ -57,7 +58,10 @@ - + + + + Idle @@ -278,6 +282,7 @@ + diff --git a/IPython/frontend/html/notebook/templates/projectdashboard.html b/IPython/frontend/html/notebook/templates/projectdashboard.html index 0b9075f..386e261 100644 --- a/IPython/frontend/html/notebook/templates/projectdashboard.html +++ b/IPython/frontend/html/notebook/templates/projectdashboard.html @@ -12,6 +12,8 @@ + +