Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

read-only notebook mode #931

Merged
merged 5 commits into from

2 participants

@minrk
Owner

As requested by @fperez

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 (the buttons, shortcuts, etc. are removed, but the requests will raise authentication errors if they manage to send the events anyway), but save/print functions are available.

No kernels are started until an authenticated user opens a notebook.

minrk added some commits
@minrk minrk Allow notebook server to run in read-only mode
Kernels are never started, and all save/delete/execution handlers raise
403: Forbidden.

/cc @fperez
da96638
@minrk minrk 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.
4e4c27a
@fperez
Owner

I think there's some JS missing, I get:

WARNING:root:404 GET /static/js/loginwidget.js (127.0.0.1) 0.47ms
@minrk
Owner

indeed, forgot to add the new file. Should be there now.

@fperez
Owner

Did you mean --read-only to be usable in the unauthenticated case at all? I just tested it, and even if I open the nb with --read-only, I still get full read-write access on the designated port. I was imagining that in the read-only flag would be useful in two different modes:

  1. No authentication: in this case, simply run a second nb on the same directory with --read-only and expose that to users.

  2. With authentication: adding --read-only allows unauthenticated users to connect as well.

I see the value in 2 when running a full-blown server, but option 1 is very lightweight and I think would be worth having.

Do you think it's difficult to get it in?

If it's hard and for now we go only with 2, then --read-only should raise an error when used without authentication, since it doesn't actually provide read-only access.

@minrk
Owner

I did 1. first, then I did 2. I'll have a peek tomorrow at if I can get 1. back in reasonably easily, otherwise I will raise an error if read-only is set without a password, as it has no effect.

@fperez
Owner

That would be awesome, thanks!

@minrk
Owner

Nevermind, it's only half a line of code to make that work. Give it a try.

@fperez
Owner

Awesome! This looks fantastic, and will be super useful. Only one thing: is it possible to have the left sidebar start closed rather than open and then quickly close? The reason I ask is that when reloading, the flashing of open/close of the sidebar is kind of annoying, and we probably expect that read-only notebooks will be reloaded very frequently in a teaching scenario. It would be nice not to have that flashing of the UI happen.

If it's not easy, we can defer it to a future point in time and merge this for now, as it's already excellent and immediately useful.

Thanks for the awesome!

@minrk
Owner

I'll see what I can do. Currently, the page load is not aware that it is read-only until the notebook request arrives, so the choice is between starting closed, and opening when logged in, and starting open, and closing when read-only. If I can move up the signal that it is read-only, then I should be able to determine the behavior before it gets drawn the first time.

In teaching scenarios, it might even make sense for it not to be hidden by default, since these would presumably be novice users, who don't know about the hidden sidebar, which holds useful links like help and save/print.

@fperez
Owner
@minrk
Owner

I'll move the read-only check to page-level, instead of on the notebook / notebooklist requests. That will allow the drawing to
make decisions immediately, rather than waiting for the delayed call.

will take a small amount of reorganization, but should be easy enough.

It's also just better, because I was not too fond of using the Allow headers to indicate read-only-ness.

@fperez
Owner
@minrk
Owner

I've got read-only attached to the pages, but I don't seem to be able to get the side panel to startup collapsed. I did prevent the contents of the panel from drawing until after it is hidden, but I couldn't get the panel itself to startup collapsed, and still work properly.

@minrk minrk move read_only flag to page-level
contents of LPanel are not drawn until after collapse

read_only is in a <meta> tag
81edd9f
@fperez
Owner
@minrk
Owner

The only thing I'm not fond of is that the login widget still exists in full read-only mode, where logging in won't do anything, but I think it's good enough to go in.

@fperez
Owner
@fperez fperez merged commit 80e60eb into from
@fperez fperez referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 25, 2011
  1. @minrk

    Allow notebook server to run in read-only mode

    minrk authored
    Kernels are never started, and all save/delete/execution handlers raise
    403: Forbidden.
    
    /cc @fperez
Commits on Oct 26, 2011
  1. @minrk

    add read-only view for notebooks

    minrk authored
    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.
Commits on Oct 28, 2011
  1. @minrk

    add missing loginwidget.js

    minrk authored
  2. @minrk
  3. @minrk

    move read_only flag to page-level

    minrk authored
    contents of LPanel are not drawn until after collapse
    
    read_only is in a <meta> tag
This page is out of date. Refresh to see the latest.
View
68 IPython/frontend/html/notebook/handlers.py
@@ -26,6 +26,7 @@
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 @@
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 @@ def get_current_user(self):
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 @@ def get(self):
'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 @@ def on_close(self):
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)
View
21 IPython/frontend/html/notebook/notebookapp.py
@@ -105,6 +105,7 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
self.log = log
self.notebook_manager = notebook_manager
self.ipython_app = ipython_app
+ self.read_only = self.ipython_app.read_only
#-----------------------------------------------------------------------------
@@ -116,11 +117,23 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
{'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)
@@ -203,6 +216,10 @@ def _ip_changed(self, name, old, new):
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 +299,7 @@ def initialize(self, argv=None):
# 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:
View
9 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;
+}
View
4 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();
View
1  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);
View
6 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');
}
View
38 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));
View
13 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 = $('<div class="end_space"></div>').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;
View
5 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);
+ }
};
};
View
25 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);
});
View
9 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.
View
4 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 = $('<div/>').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");
View
2  IPython/frontend/html/notebook/templates/login.html
@@ -11,6 +11,8 @@
<link rel="stylesheet" href="static/css/layout.css" type="text/css" />
<link rel="stylesheet" href="static/css/base.css" type="text/css" />
+ <meta name="read_only" content="{{read_only}}"/>
+
</head>
<body>
View
9 IPython/frontend/html/notebook/templates/notebook.html
@@ -40,7 +40,8 @@
<link rel="stylesheet" href="static/css/base.css" type="text/css" />
<link rel="stylesheet" href="static/css/notebook.css" type="text/css" />
<link rel="stylesheet" href="static/css/renderedhtml.css" type="text/css" />
-
+
+ <meta name="read_only" content="{{read_only}}"/>
</head>
@@ -57,7 +58,10 @@
</span>
<span id="quick_help_area">
<button id="quick_help">Quick<u>H</u>elp</button>
- </span>
+ </span>
+ <span id="login_widget" class="hidden">
+ <button id="login">Login</button>
+ </span>
<span id="kernel_status">Idle</span>
</div>
@@ -278,6 +282,7 @@
<script src="static/js/layout.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/savewidget.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/quickhelp.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/pager.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/panelsection.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/printwidget.js" type="text/javascript" charset="utf-8"></script>
View
6 IPython/frontend/html/notebook/templates/projectdashboard.html
@@ -12,6 +12,8 @@
<link rel="stylesheet" href="static/css/base.css" type="text/css" />
<link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
+ <meta name="read_only" content="{{read_only}}"/>
+
</head>
<body data-project={{project}} data-base-project-url={{base_project_url}}
@@ -19,6 +21,9 @@
<div id="header">
<span id="ipython_notebook"><h1>IPython Notebook</h1></span>
+ <span id="login_widget" class="hidden">
+ <button id="login">Login</button>
+ </span>
</div>
<div id="header_border"></div>
@@ -54,6 +59,7 @@
<script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
<script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
</body>
Something went wrong with that request. Please try again.