Skip to content
This repository

read-only notebook mode #931

Merged
merged 5 commits into from over 2 years ago

2 participants

Min RK Fernando Perez
Min RK
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.

added some commits October 24, 2011
Min RK 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
Min RK 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
Fernando Perez
Owner

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

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

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

Fernando Perez
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.

Min RK
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.

Fernando Perez
Owner

That would be awesome, thanks!

Min RK
Owner

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

Fernando Perez
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!

Min RK
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.

Fernando Perez
Owner
Min RK
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.

Fernando Perez
Owner
Min RK
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.

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

read_only is in a <meta> tag
81edd9f
Fernando Perez
Owner
Min RK
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.

Fernando Perez
Owner
Fernando Perez fperez merged commit 80e60eb into from October 28, 2011
Fernando Perez fperez closed this October 28, 2011
Fernando Perez fperez referenced this pull request from a commit January 10, 2012
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

Showing 5 unique commits by 1 author.

Oct 24, 2011
Min RK 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
Oct 25, 2011
Min RK 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
Oct 27, 2011
Min RK add missing loginwidget.js 35ab58a
Min RK allow fully read-only mode if no password is set 91d9381
Oct 28, 2011
Min RK move read_only flag to page-level
contents of LPanel are not drawn until after collapse

read_only is in a <meta> tag
81edd9f
This page is out of date. Refresh to see the latest.
68  IPython/frontend/html/notebook/handlers.py
@@ -26,6 +26,7 @@
26 26
 from zmq.eventloop import ioloop
27 27
 from zmq.utils import jsonapi
28 28
 
  29
+from IPython.external.decorator import decorator
29 30
 from IPython.zmq.session import Session
30 31
 
31 32
 try:
@@ -34,6 +35,32 @@
34 35
     publish_string = None
35 36
 
36 37
 
  38
+#-----------------------------------------------------------------------------
  39
+# Decorator for disabling read-only handlers
  40
+#-----------------------------------------------------------------------------
  41
+
  42
+@decorator
  43
+def not_if_readonly(f, self, *args, **kwargs):
  44
+    if self.application.read_only:
  45
+        raise web.HTTPError(403, "Notebook server is read-only")
  46
+    else:
  47
+        return f(self, *args, **kwargs)
  48
+
  49
+@decorator
  50
+def authenticate_unless_readonly(f, self, *args, **kwargs):
  51
+    """authenticate this page *unless* readonly view is active.
  52
+    
  53
+    In read-only mode, the notebook list and print view should
  54
+    be accessible without authentication.
  55
+    """
  56
+    
  57
+    @web.authenticated
  58
+    def auth_f(self, *args, **kwargs):
  59
+        return f(self, *args, **kwargs)
  60
+    if self.application.read_only:
  61
+        return f(self, *args, **kwargs)
  62
+    else:
  63
+        return auth_f(self, *args, **kwargs)
37 64
 
38 65
 #-----------------------------------------------------------------------------
39 66
 # Top-level handlers
@@ -50,34 +77,48 @@ def get_current_user(self):
50 77
         if user_id is None:
51 78
             # prevent extra Invalid cookie sig warnings:
52 79
             self.clear_cookie('username')
53  
-            if not self.application.password:
  80
+            if not self.application.password and not self.application.read_only:
54 81
                 user_id = 'anonymous'
55 82
         return user_id
  83
+    
  84
+    @property
  85
+    def read_only(self):
  86
+        if self.application.read_only:
  87
+            if self.application.password:
  88
+                return self.get_current_user() is None
  89
+            else:
  90
+                return True
  91
+        else:
  92
+            return False
  93
+        
56 94
 
57 95
 
58 96
 class ProjectDashboardHandler(AuthenticatedHandler):
59 97
 
60  
-    @web.authenticated
  98
+    @authenticate_unless_readonly
61 99
     def get(self):
62 100
         nbm = self.application.notebook_manager
63 101
         project = nbm.notebook_dir
64 102
         self.render(
65 103
             'projectdashboard.html', project=project,
66  
-            base_project_url=u'/', base_kernel_url=u'/'
  104
+            base_project_url=u'/', base_kernel_url=u'/',
  105
+            read_only=self.read_only,
67 106
         )
68 107
 
69 108
 
70 109
 class LoginHandler(AuthenticatedHandler):
71 110
 
72 111
     def get(self):
73  
-        self.render('login.html', next='/')
  112
+        self.render('login.html',
  113
+                next=self.get_argument('next', default='/'),
  114
+                read_only=self.read_only,
  115
+        )
74 116
 
75 117
     def post(self):
76 118
         pwd = self.get_argument('password', default=u'')
77 119
         if self.application.password and pwd == self.application.password:
78 120
             self.set_secure_cookie('username', str(uuid.uuid4()))
79  
-        url = self.get_argument('next', default='/')
80  
-        self.redirect(url)
  121
+        self.redirect(self.get_argument('next', default='/'))
81 122
 
82 123
 
83 124
 class NewHandler(AuthenticatedHandler):
@@ -91,23 +132,26 @@ def get(self):
91 132
             'notebook.html', project=project,
92 133
             notebook_id=notebook_id,
93 134
             base_project_url=u'/', base_kernel_url=u'/',
94  
-            kill_kernel=False
  135
+            kill_kernel=False,
  136
+            read_only=False,
95 137
         )
96 138
 
97 139
 
98 140
 class NamedNotebookHandler(AuthenticatedHandler):
99 141
 
100  
-    @web.authenticated
  142
+    @authenticate_unless_readonly
101 143
     def get(self, notebook_id):
102 144
         nbm = self.application.notebook_manager
103 145
         project = nbm.notebook_dir
104 146
         if not nbm.notebook_exists(notebook_id):
105 147
             raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
  148
+        
106 149
         self.render(
107 150
             'notebook.html', project=project,
108 151
             notebook_id=notebook_id,
109 152
             base_project_url=u'/', base_kernel_url=u'/',
110  
-            kill_kernel=False
  153
+            kill_kernel=False,
  154
+            read_only=self.read_only,
111 155
         )
112 156
 
113 157
 
@@ -363,8 +407,9 @@ def on_close(self):
363 407
 
364 408
 class NotebookRootHandler(AuthenticatedHandler):
365 409
 
366  
-    @web.authenticated
  410
+    @authenticate_unless_readonly
367 411
     def get(self):
  412
+        
368 413
         nbm = self.application.notebook_manager
369 414
         files = nbm.list_notebooks()
370 415
         self.finish(jsonapi.dumps(files))
@@ -387,11 +432,12 @@ class NotebookHandler(AuthenticatedHandler):
387 432
 
388 433
     SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
389 434
 
390  
-    @web.authenticated
  435
+    @authenticate_unless_readonly
391 436
     def get(self, notebook_id):
392 437
         nbm = self.application.notebook_manager
393 438
         format = self.get_argument('format', default='json')
394 439
         last_mod, name, data = nbm.get_notebook(notebook_id, format)
  440
+        
395 441
         if format == u'json':
396 442
             self.set_header('Content-Type', 'application/json')
397 443
             self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
21  IPython/frontend/html/notebook/notebookapp.py
@@ -105,6 +105,7 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
105 105
         self.log = log
106 106
         self.notebook_manager = notebook_manager
107 107
         self.ipython_app = ipython_app
  108
+        self.read_only = self.ipython_app.read_only
108 109
 
109 110
 
110 111
 #-----------------------------------------------------------------------------
@@ -116,11 +117,23 @@ def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
116 117
     {'NotebookApp' : {'open_browser' : False}},
117 118
     "Don't open the notebook in a browser after startup."
118 119
 )
  120
+flags['read-only'] = (
  121
+    {'NotebookApp' : {'read_only' : True}},
  122
+    """Allow read-only access to notebooks.
  123
+    
  124
+    When using a password to protect the notebook server, this flag
  125
+    allows unauthenticated clients to view the notebook list, and
  126
+    individual notebooks, but not edit them, start kernels, or run
  127
+    code.
  128
+    
  129
+    If no password is set, the server will be entirely read-only.
  130
+    """
  131
+)
119 132
 
120 133
 # the flags that are specific to the frontend
121 134
 # these must be scrubbed before being passed to the kernel,
122 135
 # or it will raise an error on unrecognized flags
123  
-notebook_flags = ['no-browser']
  136
+notebook_flags = ['no-browser', 'read-only']
124 137
 
125 138
 aliases = dict(ipkernel_aliases)
126 139
 
@@ -203,6 +216,10 @@ def _ip_changed(self, name, old, new):
203 216
     
204 217
     open_browser = Bool(True, config=True,
205 218
                         help="Whether to open in a browser after starting.")
  219
+    
  220
+    read_only = Bool(False, config=True,
  221
+        help="Whether to prevent editing/execution of notebooks."
  222
+    )
206 223
 
207 224
     def get_ws_url(self):
208 225
         """Return the WebSocket URL for this server."""
@@ -282,7 +299,7 @@ def initialize(self, argv=None):
282 299
         # Try random ports centered around the default.
283 300
         from random import randint
284 301
         n = 50  # Max number of attempts, keep reasonably large.
285  
-        for port in [self.port] + [self.port + randint(-2*n, 2*n) for i in range(n)]:
  302
+        for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
286 303
             try:
287 304
                 self.http_server.listen(port, self.ip)
288 305
             except socket.error, e:
9  IPython/frontend/html/notebook/static/css/base.css
@@ -51,3 +51,12 @@ div#main_app {
51 51
     padding: 0.2em 0.8em;
52 52
     font-size: 77%;
53 53
 }
  54
+
  55
+span#login_widget {
  56
+    float: right;
  57
+}
  58
+
  59
+/* generic class for hidden objects */
  60
+.hidden {
  61
+    display: none;
  62
+}
4  IPython/frontend/html/notebook/static/js/cell.js
@@ -15,6 +15,10 @@ var IPython = (function (IPython) {
15 15
 
16 16
     var Cell = function (notebook) {
17 17
         this.notebook = notebook;
  18
+        this.read_only = false;
  19
+        if (notebook){
  20
+            this.read_only = notebook.read_only;
  21
+        }
18 22
         this.selected = false;
19 23
         this.element = null;
20 24
         this.create_element();
1  IPython/frontend/html/notebook/static/js/codecell.js
@@ -37,6 +37,7 @@ var IPython = (function (IPython) {
37 37
             indentUnit : 4,
38 38
             mode: 'python',
39 39
             theme: 'ipython',
  40
+            readOnly: this.read_only,
40 41
             onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
41 42
         });
42 43
         input.append(input_area);
6  IPython/frontend/html/notebook/static/js/leftpanel.js
@@ -65,8 +65,10 @@ var IPython = (function (IPython) {
65 65
 
66 66
     LeftPanel.prototype.create_children = function () {
67 67
         this.notebook_section = new IPython.NotebookSection('div#notebook_section');
68  
-        this.cell_section = new IPython.CellSection('div#cell_section');
69  
-        this.kernel_section = new IPython.KernelSection('div#kernel_section');
  68
+        if (! IPython.read_only){
  69
+            this.cell_section = new IPython.CellSection('div#cell_section');
  70
+            this.kernel_section = new IPython.KernelSection('div#kernel_section');
  71
+        }
70 72
         this.help_section = new IPython.HelpSection('div#help_section');
71 73
     }
72 74
 
38  IPython/frontend/html/notebook/static/js/loginwidget.js
... ...
@@ -0,0 +1,38 @@
  1
+//----------------------------------------------------------------------------
  2
+//  Copyright (C) 2008-2011  The IPython Development Team
  3
+//
  4
+//  Distributed under the terms of the BSD License.  The full license is in
  5
+//  the file COPYING, distributed as part of this software.
  6
+//----------------------------------------------------------------------------
  7
+
  8
+//============================================================================
  9
+// Login button
  10
+//============================================================================
  11
+
  12
+var IPython = (function (IPython) {
  13
+
  14
+    var LoginWidget = function (selector) {
  15
+        this.selector = selector;
  16
+        if (this.selector !== undefined) {
  17
+            this.element = $(selector);
  18
+            this.style();
  19
+            this.bind_events();
  20
+        }
  21
+    };
  22
+
  23
+    LoginWidget.prototype.style = function () {
  24
+        this.element.find('button#login').button();
  25
+    };
  26
+    LoginWidget.prototype.bind_events = function () {
  27
+        var that = this;
  28
+        this.element.find("button#login").click(function () {
  29
+            window.location = "/login?next="+location.pathname;
  30
+        });
  31
+    };
  32
+
  33
+    // Set module variables
  34
+    IPython.LoginWidget = LoginWidget;
  35
+
  36
+    return IPython;
  37
+
  38
+}(IPython));
13  IPython/frontend/html/notebook/static/js/notebook.js
@@ -14,6 +14,7 @@ var IPython = (function (IPython) {
14 14
     var utils = IPython.utils;
15 15
 
16 16
     var Notebook = function (selector) {
  17
+        this.read_only = IPython.read_only;
17 18
         this.element = $(selector);
18 19
         this.element.scroll();
19 20
         this.element.data("notebook", this);
@@ -42,6 +43,7 @@ var IPython = (function (IPython) {
42 43
         var that = this;
43 44
         var end_space = $('<div class="end_space"></div>').height(150);
44 45
         end_space.dblclick(function (e) {
  46
+            if (that.read_only) return;
45 47
             var ncells = that.ncells();
46 48
             that.insert_code_cell_below(ncells-1);
47 49
         });
@@ -54,6 +56,7 @@ var IPython = (function (IPython) {
54 56
         var that = this;
55 57
         $(document).keydown(function (event) {
56 58
             // console.log(event);
  59
+            if (that.read_only) return;
57 60
             if (event.which === 38) {
58 61
                 var cell = that.selected_cell();
59 62
                 if (cell.at_top()) {
@@ -185,11 +188,11 @@ var IPython = (function (IPython) {
185 188
         });
186 189
 
187 190
         $(window).bind('beforeunload', function () {
188  
-            var kill_kernel = $('#kill_kernel').prop('checked');                
  191
+            var kill_kernel = $('#kill_kernel').prop('checked');
189 192
             if (kill_kernel) {
190 193
                 that.kernel.kill();
191 194
             }
192  
-            if (that.dirty) {
  195
+            if (that.dirty && ! that.read_only) {
193 196
                 return "You have unsaved changes that will be lost if you leave this page.";
194 197
             };
195 198
         });
@@ -975,14 +978,17 @@ var IPython = (function (IPython) {
975 978
 
976 979
 
977 980
     Notebook.prototype.notebook_loaded = function (data, status, xhr) {
  981
+        var allowed = xhr.getResponseHeader('Allow');
978 982
         this.fromJSON(data);
979 983
         if (this.ncells() === 0) {
980 984
             this.insert_code_cell_below();
981 985
         };
982 986
         IPython.save_widget.status_save();
983 987
         IPython.save_widget.set_notebook_name(data.metadata.name);
984  
-        this.start_kernel();
985 988
         this.dirty = false;
  989
+        if (! this.read_only) {
  990
+            this.start_kernel();
  991
+        }
986 992
         // fromJSON always selects the last cell inserted. We need to wait
987 993
         // until that is done before scrolling to the top.
988 994
         setTimeout(function () {
@@ -991,7 +997,6 @@ var IPython = (function (IPython) {
991 997
         }, 50);
992 998
     };
993 999
 
994  
-
995 1000
     IPython.Notebook = Notebook;
996 1001
 
997 1002
 
5  IPython/frontend/html/notebook/static/js/notebooklist.js
@@ -80,7 +80,10 @@ var IPython = (function (IPython) {
80 80
             var nbname = data[i].name;
81 81
             var item = this.new_notebook_item(i);
82 82
             this.add_link(notebook_id, nbname, item);
83  
-            this.add_delete_button(item);
  83
+            if (!IPython.read_only){
  84
+                // hide delete buttons when readonly
  85
+                this.add_delete_button(item);
  86
+            }
84 87
         };
85 88
     };
86 89
 
25  IPython/frontend/html/notebook/static/js/notebookmain.js
@@ -23,6 +23,7 @@ $(document).ready(function () {
23 23
         }
24 24
     });
25 25
     IPython.markdown_converter = new Markdown.Converter();
  26
+    IPython.read_only = $('meta[name=read_only]').attr("content") == 'True';
26 27
 
27 28
     $('div#header').addClass('border-box-sizing');
28 29
     $('div#main_app').addClass('border-box-sizing ui-widget ui-widget-content');
@@ -33,6 +34,7 @@ $(document).ready(function () {
33 34
     IPython.left_panel = new IPython.LeftPanel('div#left_panel', 'div#left_panel_splitter');
34 35
     IPython.save_widget = new IPython.SaveWidget('span#save_widget');
35 36
     IPython.quick_help = new IPython.QuickHelp('span#quick_help_area');
  37
+    IPython.login_widget = new IPython.LoginWidget('span#login_widget');
36 38
     IPython.print_widget = new IPython.PrintWidget('span#print_widget');
37 39
     IPython.notebook = new IPython.Notebook('div#notebook');
38 40
     IPython.kernel_status_widget = new IPython.KernelStatusWidget('#kernel_status');
@@ -42,6 +44,21 @@ $(document).ready(function () {
42 44
 
43 45
     // These have display: none in the css file and are made visible here to prevent FLOUC.
44 46
     $('div#header').css('display','block');
  47
+
  48
+    if(IPython.read_only){
  49
+        // hide various elements from read-only view
  50
+        IPython.save_widget.element.find('button#save_notebook').addClass('hidden');
  51
+        IPython.quick_help.element.addClass('hidden'); // shortcuts are disabled in read_only
  52
+        $('button#new_notebook').addClass('hidden');
  53
+        $('div#cell_section').addClass('hidden');
  54
+        $('div#kernel_section').addClass('hidden');
  55
+        $('span#login_widget').removeClass('hidden');
  56
+        // left panel starts collapsed, but the collapse must happen after
  57
+        // elements start drawing.  Don't draw contents of the panel until
  58
+        // after they are collapsed
  59
+        IPython.left_panel.left_panel_element.css('visibility', 'hidden');
  60
+    }
  61
+
45 62
     $('div#main_app').css('display','block');
46 63
 
47 64
     // Perform these actions after the notebook has been loaded.
@@ -52,6 +69,14 @@ $(document).ready(function () {
52 69
             IPython.save_widget.update_url();
53 70
             IPython.layout_manager.do_resize();
54 71
             IPython.pager.collapse();
  72
+            if(IPython.read_only){
  73
+                // collapse the left panel on read-only
  74
+                IPython.left_panel.collapse();
  75
+                // and finally unhide the panel contents after collapse
  76
+                setTimeout(function(){
  77
+                    IPython.left_panel.left_panel_element.css('visibility', 'visible');
  78
+                }, 200)
  79
+            }
55 80
         },100);
56 81
     });
57 82
 
9  IPython/frontend/html/notebook/static/js/projectdashboardmain.js
@@ -27,7 +27,16 @@ $(document).ready(function () {
27 27
     $('div#left_panel').addClass('box-flex');
28 28
     $('div#right_panel').addClass('box-flex');
29 29
 
  30
+    IPython.read_only = $('meta[name=read_only]').attr("content") == 'True';
  31
+    
30 32
     IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
  33
+    IPython.login_widget = new IPython.LoginWidget('span#login_widget');
  34
+    
  35
+    if (IPython.read_only){
  36
+        $('#new_notebook').addClass('hidden');
  37
+        // unhide login button if it's relevant
  38
+        $('span#login_widget').removeClass('hidden');
  39
+    }
31 40
     IPython.notebook_list.load_list();
32 41
 
33 42
     // These have display: none in the css file and are made visible here to prevent FLOUC.
4  IPython/frontend/html/notebook/static/js/textcell.js
@@ -33,7 +33,8 @@ var IPython = (function (IPython) {
33 33
             indentUnit : 4,
34 34
             mode: this.code_mirror_mode,
35 35
             theme: 'default',
36  
-            value: this.placeholder
  36
+            value: this.placeholder,
  37
+            readOnly: this.read_only,
37 38
         });
38 39
         // The tabindex=-1 makes this div focusable.
39 40
         var render_area = $('<div/>').addClass('text_cell_render').
@@ -65,6 +66,7 @@ var IPython = (function (IPython) {
65 66
 
66 67
 
67 68
     TextCell.prototype.edit = function () {
  69
+        if ( this.read_only ) return;
68 70
         if (this.rendered === true) {
69 71
             var text_cell = this.element;
70 72
             var output = text_cell.find("div.text_cell_render");  
2  IPython/frontend/html/notebook/templates/login.html
@@ -11,6 +11,8 @@
11 11
     <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 12
     <link rel="stylesheet" href="static/css/base.css" type="text/css" />
13 13
 
  14
+    <meta name="read_only" content="{{read_only}}"/>
  15
+
14 16
 </head>
15 17
 
16 18
 <body>
9  IPython/frontend/html/notebook/templates/notebook.html
@@ -40,7 +40,8 @@
40 40
     <link rel="stylesheet" href="static/css/base.css" type="text/css" />
41 41
     <link rel="stylesheet" href="static/css/notebook.css" type="text/css" />
42 42
     <link rel="stylesheet" href="static/css/renderedhtml.css" type="text/css" />
43  
-
  43
+    
  44
+    <meta name="read_only" content="{{read_only}}"/>
44 45
 
45 46
 </head>
46 47
 
@@ -57,7 +58,10 @@
57 58
     </span>
58 59
     <span id="quick_help_area">
59 60
       <button id="quick_help">Quick<u>H</u>elp</button>
60  
-      </span>
  61
+    </span>
  62
+    <span id="login_widget" class="hidden">
  63
+      <button id="login">Login</button>
  64
+    </span>
61 65
     <span id="kernel_status">Idle</span>
62 66
 </div>
63 67
 
@@ -278,6 +282,7 @@
278 282
 <script src="static/js/layout.js" type="text/javascript" charset="utf-8"></script>
279 283
 <script src="static/js/savewidget.js" type="text/javascript" charset="utf-8"></script>
280 284
 <script src="static/js/quickhelp.js" type="text/javascript" charset="utf-8"></script>
  285
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
281 286
 <script src="static/js/pager.js" type="text/javascript" charset="utf-8"></script>
282 287
 <script src="static/js/panelsection.js" type="text/javascript" charset="utf-8"></script>
283 288
 <script src="static/js/printwidget.js" type="text/javascript" charset="utf-8"></script>
6  IPython/frontend/html/notebook/templates/projectdashboard.html
@@ -12,6 +12,8 @@
12 12
     <link rel="stylesheet" href="static/css/base.css" type="text/css" />
13 13
     <link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
14 14
 
  15
+    <meta name="read_only" content="{{read_only}}"/>
  16
+
15 17
 </head>
16 18
 
17 19
 <body data-project={{project}} data-base-project-url={{base_project_url}}
@@ -19,6 +21,9 @@
19 21
 
20 22
 <div id="header">
21 23
     <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
  24
+    <span id="login_widget" class="hidden">
  25
+      <button id="login">Login</button>
  26
+    </span>
22 27
 </div>
23 28
 
24 29
 <div id="header_border"></div>
@@ -54,6 +59,7 @@
54 59
 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
55 60
 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
56 61
 <script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
  62
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
57 63
 <script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
58 64
 
59 65
 </body>
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.