mirror of
https://github.com/jupyter/notebook.git
synced 2025-02-23 12:49:41 +08:00
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.
This commit is contained in:
parent
9a7fda926d
commit
a6de5947de
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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 = $('<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,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;
|
||||
|
||||
|
||||
|
@ -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<len; i++) {
|
||||
@ -80,7 +89,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 (!this.read_only){
|
||||
// hide delete buttons when readonly
|
||||
this.add_delete_button(item);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -33,6 +33,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');
|
||||
|
@ -28,6 +28,7 @@ $(document).ready(function () {
|
||||
$('div#right_panel').addClass('box-flex');
|
||||
|
||||
IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
|
||||
IPython.login_widget = new IPython.LoginWidget('span#login_widget');
|
||||
IPython.notebook_list.load_list();
|
||||
|
||||
// These have display: none in the css file and are made visible here to prevent FLOUC.
|
||||
|
@ -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");
|
||||
|
@ -57,7 +57,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 +281,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>
|
||||
|
@ -19,6 +19,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 +57,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>
|
||||
|
Loading…
Reference in New Issue
Block a user