##// END OF EJS Templates
Merge branch 'logout_button' of https://github.com/stefanv/ipython into stefanv-logout_button...
Fernando Perez -
r5359:80e7338b merge
parent child Browse files
Show More
@@ -0,0 +1,77 b''
1 <!DOCTYPE HTML>
2 <html>
3
4 <head>
5 <meta charset="utf-8">
6
7 <title>{% block title %}IPython Notebook{% end %}</title>
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css"/>
13 {% block stylesheet %}
14 {% end %}
15
16 {% block meta %}
17 {% end %}
18
19 </head>
20
21 <body {% block params %}{% end %}>
22
23 <div id="header">
24 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
25 <span id="login_widget">
26 {% if current_user and current_user != 'anonymous' %}
27 <button id="logout">Logout</button>
28 {% end %}
29 </span>
30 {% block header %}
31 {% end %}
32 </div>
33
34 <div id="header_border"></div>
35
36 <div id="main_app">
37
38 <div id="app_hbox">
39
40 <div id="left_panel">
41 {% block left_panel %}
42 {% end %}
43 </div>
44
45 <div id="content_panel">
46 {% if message %}
47
48 {% for key in message %}
49 <div class="message {{key}}">
50 {{message[key]}}
51 </div>
52 {% end %}
53 {% end %}
54
55 {% block content_panel %}
56 {% end %}
57 </div>
58 <div id="right_panel">
59 {% block right_panel %}
60 {% end %}
61 </div>
62
63 </div>
64
65 </div>
66
67 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
68 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
69 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
70 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
71 <script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
72 {% block script %}
73 {% end %}
74
75 </body>
76
77 </html>
@@ -0,0 +1,5 b''
1 {% extends layout.html %}
2
3 {% block content_panel %}
4 Proceed to the <a href="/login">login page</a>.
5 {% end %}
@@ -1,562 +1,579 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import logging
19 import logging
20 import Cookie
20 import Cookie
21 import uuid
21 import uuid
22
22
23 from tornado import web
23 from tornado import web
24 from tornado import websocket
24 from tornado import websocket
25
25
26 from zmq.eventloop import ioloop
26 from zmq.eventloop import ioloop
27 from zmq.utils import jsonapi
27 from zmq.utils import jsonapi
28
28
29 from IPython.external.decorator import decorator
29 from IPython.external.decorator import decorator
30 from IPython.zmq.session import Session
30 from IPython.zmq.session import Session
31 from IPython.lib.security import passwd_check
31 from IPython.lib.security import passwd_check
32
32
33 try:
33 try:
34 from docutils.core import publish_string
34 from docutils.core import publish_string
35 except ImportError:
35 except ImportError:
36 publish_string = None
36 publish_string = None
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # parts tornado cares about haven't really changed, so it's OK to continue
43 # parts tornado cares about haven't really changed, so it's OK to continue
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # version as of Oct 30/2011) the version check fails, see the issue report:
45 # version as of Oct 30/2011) the version check fails, see the issue report:
46
46
47 # https://github.com/facebook/tornado/issues/385
47 # https://github.com/facebook/tornado/issues/385
48
48
49 # This issue has been fixed in Tornado post 2.1.1:
49 # This issue has been fixed in Tornado post 2.1.1:
50
50
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52
52
53 # Here we manually apply the same patch as above so that users of IPython can
53 # Here we manually apply the same patch as above so that users of IPython can
54 # continue to work with an officially released Tornado. We make the
54 # continue to work with an officially released Tornado. We make the
55 # monkeypatch version check as narrow as possible to limit its effects; once
55 # monkeypatch version check as narrow as possible to limit its effects; once
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57
57
58 import tornado
58 import tornado
59
59
60 if tornado.version_info <= (2,1,1):
60 if tornado.version_info <= (2,1,1):
61
61
62 def _execute(self, transforms, *args, **kwargs):
62 def _execute(self, transforms, *args, **kwargs):
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64
64
65 self.open_args = args
65 self.open_args = args
66 self.open_kwargs = kwargs
66 self.open_kwargs = kwargs
67
67
68 # The difference between version 8 and 13 is that in 8 the
68 # The difference between version 8 and 13 is that in 8 the
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # simply "Origin".
70 # simply "Origin".
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 self.ws_connection = WebSocketProtocol8(self)
72 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection.accept_connection()
73 self.ws_connection.accept_connection()
74
74
75 elif self.request.headers.get("Sec-WebSocket-Version"):
75 elif self.request.headers.get("Sec-WebSocket-Version"):
76 self.stream.write(tornado.escape.utf8(
76 self.stream.write(tornado.escape.utf8(
77 "HTTP/1.1 426 Upgrade Required\r\n"
77 "HTTP/1.1 426 Upgrade Required\r\n"
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 self.stream.close()
79 self.stream.close()
80
80
81 else:
81 else:
82 self.ws_connection = WebSocketProtocol76(self)
82 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection.accept_connection()
83 self.ws_connection.accept_connection()
84
84
85 websocket.WebSocketHandler._execute = _execute
85 websocket.WebSocketHandler._execute = _execute
86 del _execute
86 del _execute
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Decorator for disabling read-only handlers
89 # Decorator for disabling read-only handlers
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 @decorator
92 @decorator
93 def not_if_readonly(f, self, *args, **kwargs):
93 def not_if_readonly(f, self, *args, **kwargs):
94 if self.application.read_only:
94 if self.application.read_only:
95 raise web.HTTPError(403, "Notebook server is read-only")
95 raise web.HTTPError(403, "Notebook server is read-only")
96 else:
96 else:
97 return f(self, *args, **kwargs)
97 return f(self, *args, **kwargs)
98
98
99 @decorator
99 @decorator
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 """authenticate this page *unless* readonly view is active.
101 """authenticate this page *unless* readonly view is active.
102
102
103 In read-only mode, the notebook list and print view should
103 In read-only mode, the notebook list and print view should
104 be accessible without authentication.
104 be accessible without authentication.
105 """
105 """
106
106
107 @web.authenticated
107 @web.authenticated
108 def auth_f(self, *args, **kwargs):
108 def auth_f(self, *args, **kwargs):
109 return f(self, *args, **kwargs)
109 return f(self, *args, **kwargs)
110 if self.application.read_only:
110 if self.application.read_only:
111 return f(self, *args, **kwargs)
111 return f(self, *args, **kwargs)
112 else:
112 else:
113 return auth_f(self, *args, **kwargs)
113 return auth_f(self, *args, **kwargs)
114
114
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116 # Top-level handlers
116 # Top-level handlers
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118
118
119 class AuthenticatedHandler(web.RequestHandler):
119 class RequestHandler(web.RequestHandler):
120 """RequestHandler with default variable setting."""
121
122 def render(*args, **kwargs):
123 kwargs.setdefault('message', '')
124 return web.RequestHandler.render(*args, **kwargs)
125
126 class AuthenticatedHandler(RequestHandler):
120 """A RequestHandler with an authenticated user."""
127 """A RequestHandler with an authenticated user."""
121
128
122 def get_current_user(self):
129 def get_current_user(self):
123 user_id = self.get_secure_cookie("username")
130 user_id = self.get_secure_cookie("username")
124 # For now the user_id should not return empty, but it could eventually
131 # For now the user_id should not return empty, but it could eventually
125 if user_id == '':
132 if user_id == '':
126 user_id = 'anonymous'
133 user_id = 'anonymous'
127 if user_id is None:
134 if user_id is None:
128 # prevent extra Invalid cookie sig warnings:
135 # prevent extra Invalid cookie sig warnings:
129 self.clear_cookie('username')
136 self.clear_cookie('username')
130 if not self.application.password and not self.application.read_only:
137 if not self.application.password and not self.application.read_only:
131 user_id = 'anonymous'
138 user_id = 'anonymous'
132 return user_id
139 return user_id
133
140
134 @property
141 @property
135 def read_only(self):
142 def read_only(self):
136 if self.application.read_only:
143 if self.application.read_only:
137 if self.application.password:
144 if self.application.password:
138 return self.get_current_user() is None
145 return self.get_current_user() is None
139 else:
146 else:
140 return True
147 return True
141 else:
148 else:
142 return False
149 return False
143
150
144 @property
151 @property
145 def ws_url(self):
152 def ws_url(self):
146 """websocket url matching the current request
153 """websocket url matching the current request
147
154
148 turns http[s]://host[:port] into
155 turns http[s]://host[:port] into
149 ws[s]://host[:port]
156 ws[s]://host[:port]
150 """
157 """
151 proto = self.request.protocol.replace('http', 'ws')
158 proto = self.request.protocol.replace('http', 'ws')
152 return "%s://%s" % (proto, self.request.host)
159 return "%s://%s" % (proto, self.request.host)
153
160
154
161
155 class ProjectDashboardHandler(AuthenticatedHandler):
162 class ProjectDashboardHandler(AuthenticatedHandler):
156
163
157 @authenticate_unless_readonly
164 @authenticate_unless_readonly
158 def get(self):
165 def get(self):
159 nbm = self.application.notebook_manager
166 nbm = self.application.notebook_manager
160 project = nbm.notebook_dir
167 project = nbm.notebook_dir
161 self.render(
168 self.render(
162 'projectdashboard.html', project=project,
169 'projectdashboard.html', project=project,
163 base_project_url=u'/', base_kernel_url=u'/',
170 base_project_url=u'/', base_kernel_url=u'/',
164 read_only=self.read_only,
171 read_only=self.read_only,
165 )
172 )
166
173
167
174
168 class LoginHandler(AuthenticatedHandler):
175 class LoginHandler(AuthenticatedHandler):
169
176
170 def _render(self, message=''):
177 def _render(self, message=None):
171 self.render('login.html',
178 self.render('login.html',
172 next=self.get_argument('next', default='/'),
179 next=self.get_argument('next', default='/'),
173 read_only=self.read_only,
180 read_only=self.read_only,
174 message=message
181 message=message
175 )
182 )
176
183
177 def get(self):
184 def get(self):
178 self._render()
185 if self.current_user:
186 self.redirect(self.get_argument('next', default='/'))
187 else:
188 self._render()
179
189
180 def post(self):
190 def post(self):
181 pwd = self.get_argument('password', default=u'')
191 pwd = self.get_argument('password', default=u'')
182 if self.application.password:
192 if self.application.password:
183 if passwd_check(self.application.password, pwd):
193 if passwd_check(self.application.password, pwd):
184 self.set_secure_cookie('username', str(uuid.uuid4()))
194 self.set_secure_cookie('username', str(uuid.uuid4()))
185 else:
195 else:
186 self._render(message='Invalid password')
196 self._render(message={'error': 'Invalid password'})
187 return
197 return
188
198
189 self.redirect(self.get_argument('next', default='/'))
199 self.redirect(self.get_argument('next', default='/'))
190
200
191
201
202 class LogoutHandler(AuthenticatedHandler):
203
204 def get(self):
205 self.clear_cookie('username')
206 self.render('logout.html', message={'info': 'Successfully logged out.'})
207
208
192 class NewHandler(AuthenticatedHandler):
209 class NewHandler(AuthenticatedHandler):
193
210
194 @web.authenticated
211 @web.authenticated
195 def get(self):
212 def get(self):
196 nbm = self.application.notebook_manager
213 nbm = self.application.notebook_manager
197 project = nbm.notebook_dir
214 project = nbm.notebook_dir
198 notebook_id = nbm.new_notebook()
215 notebook_id = nbm.new_notebook()
199 self.render(
216 self.render(
200 'notebook.html', project=project,
217 'notebook.html', project=project,
201 notebook_id=notebook_id,
218 notebook_id=notebook_id,
202 base_project_url=u'/', base_kernel_url=u'/',
219 base_project_url=u'/', base_kernel_url=u'/',
203 kill_kernel=False,
220 kill_kernel=False,
204 read_only=False,
221 read_only=False,
205 )
222 )
206
223
207
224
208 class NamedNotebookHandler(AuthenticatedHandler):
225 class NamedNotebookHandler(AuthenticatedHandler):
209
226
210 @authenticate_unless_readonly
227 @authenticate_unless_readonly
211 def get(self, notebook_id):
228 def get(self, notebook_id):
212 nbm = self.application.notebook_manager
229 nbm = self.application.notebook_manager
213 project = nbm.notebook_dir
230 project = nbm.notebook_dir
214 if not nbm.notebook_exists(notebook_id):
231 if not nbm.notebook_exists(notebook_id):
215 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
232 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
216
233
217 self.render(
234 self.render(
218 'notebook.html', project=project,
235 'notebook.html', project=project,
219 notebook_id=notebook_id,
236 notebook_id=notebook_id,
220 base_project_url=u'/', base_kernel_url=u'/',
237 base_project_url=u'/', base_kernel_url=u'/',
221 kill_kernel=False,
238 kill_kernel=False,
222 read_only=self.read_only,
239 read_only=self.read_only,
223 )
240 )
224
241
225
242
226 #-----------------------------------------------------------------------------
243 #-----------------------------------------------------------------------------
227 # Kernel handlers
244 # Kernel handlers
228 #-----------------------------------------------------------------------------
245 #-----------------------------------------------------------------------------
229
246
230
247
231 class MainKernelHandler(AuthenticatedHandler):
248 class MainKernelHandler(AuthenticatedHandler):
232
249
233 @web.authenticated
250 @web.authenticated
234 def get(self):
251 def get(self):
235 km = self.application.kernel_manager
252 km = self.application.kernel_manager
236 self.finish(jsonapi.dumps(km.kernel_ids))
253 self.finish(jsonapi.dumps(km.kernel_ids))
237
254
238 @web.authenticated
255 @web.authenticated
239 def post(self):
256 def post(self):
240 km = self.application.kernel_manager
257 km = self.application.kernel_manager
241 notebook_id = self.get_argument('notebook', default=None)
258 notebook_id = self.get_argument('notebook', default=None)
242 kernel_id = km.start_kernel(notebook_id)
259 kernel_id = km.start_kernel(notebook_id)
243 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
260 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
244 self.set_header('Location', '/'+kernel_id)
261 self.set_header('Location', '/'+kernel_id)
245 self.finish(jsonapi.dumps(data))
262 self.finish(jsonapi.dumps(data))
246
263
247
264
248 class KernelHandler(AuthenticatedHandler):
265 class KernelHandler(AuthenticatedHandler):
249
266
250 SUPPORTED_METHODS = ('DELETE')
267 SUPPORTED_METHODS = ('DELETE')
251
268
252 @web.authenticated
269 @web.authenticated
253 def delete(self, kernel_id):
270 def delete(self, kernel_id):
254 km = self.application.kernel_manager
271 km = self.application.kernel_manager
255 km.kill_kernel(kernel_id)
272 km.kill_kernel(kernel_id)
256 self.set_status(204)
273 self.set_status(204)
257 self.finish()
274 self.finish()
258
275
259
276
260 class KernelActionHandler(AuthenticatedHandler):
277 class KernelActionHandler(AuthenticatedHandler):
261
278
262 @web.authenticated
279 @web.authenticated
263 def post(self, kernel_id, action):
280 def post(self, kernel_id, action):
264 km = self.application.kernel_manager
281 km = self.application.kernel_manager
265 if action == 'interrupt':
282 if action == 'interrupt':
266 km.interrupt_kernel(kernel_id)
283 km.interrupt_kernel(kernel_id)
267 self.set_status(204)
284 self.set_status(204)
268 if action == 'restart':
285 if action == 'restart':
269 new_kernel_id = km.restart_kernel(kernel_id)
286 new_kernel_id = km.restart_kernel(kernel_id)
270 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
287 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
271 self.set_header('Location', '/'+new_kernel_id)
288 self.set_header('Location', '/'+new_kernel_id)
272 self.write(jsonapi.dumps(data))
289 self.write(jsonapi.dumps(data))
273 self.finish()
290 self.finish()
274
291
275
292
276 class ZMQStreamHandler(websocket.WebSocketHandler):
293 class ZMQStreamHandler(websocket.WebSocketHandler):
277
294
278 def _reserialize_reply(self, msg_list):
295 def _reserialize_reply(self, msg_list):
279 """Reserialize a reply message using JSON.
296 """Reserialize a reply message using JSON.
280
297
281 This takes the msg list from the ZMQ socket, unserializes it using
298 This takes the msg list from the ZMQ socket, unserializes it using
282 self.session and then serializes the result using JSON. This method
299 self.session and then serializes the result using JSON. This method
283 should be used by self._on_zmq_reply to build messages that can
300 should be used by self._on_zmq_reply to build messages that can
284 be sent back to the browser.
301 be sent back to the browser.
285 """
302 """
286 idents, msg_list = self.session.feed_identities(msg_list)
303 idents, msg_list = self.session.feed_identities(msg_list)
287 msg = self.session.unserialize(msg_list)
304 msg = self.session.unserialize(msg_list)
288 try:
305 try:
289 msg['header'].pop('date')
306 msg['header'].pop('date')
290 except KeyError:
307 except KeyError:
291 pass
308 pass
292 try:
309 try:
293 msg['parent_header'].pop('date')
310 msg['parent_header'].pop('date')
294 except KeyError:
311 except KeyError:
295 pass
312 pass
296 msg.pop('buffers')
313 msg.pop('buffers')
297 return jsonapi.dumps(msg)
314 return jsonapi.dumps(msg)
298
315
299 def _on_zmq_reply(self, msg_list):
316 def _on_zmq_reply(self, msg_list):
300 try:
317 try:
301 msg = self._reserialize_reply(msg_list)
318 msg = self._reserialize_reply(msg_list)
302 except:
319 except:
303 self.application.log.critical("Malformed message: %r" % msg_list)
320 self.application.log.critical("Malformed message: %r" % msg_list)
304 else:
321 else:
305 self.write_message(msg)
322 self.write_message(msg)
306
323
307
324
308 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
325 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
309
326
310 def open(self, kernel_id):
327 def open(self, kernel_id):
311 self.kernel_id = kernel_id.decode('ascii')
328 self.kernel_id = kernel_id.decode('ascii')
312 try:
329 try:
313 cfg = self.application.ipython_app.config
330 cfg = self.application.ipython_app.config
314 except AttributeError:
331 except AttributeError:
315 # protect from the case where this is run from something other than
332 # protect from the case where this is run from something other than
316 # the notebook app:
333 # the notebook app:
317 cfg = None
334 cfg = None
318 self.session = Session(config=cfg)
335 self.session = Session(config=cfg)
319 self.save_on_message = self.on_message
336 self.save_on_message = self.on_message
320 self.on_message = self.on_first_message
337 self.on_message = self.on_first_message
321
338
322 def get_current_user(self):
339 def get_current_user(self):
323 user_id = self.get_secure_cookie("username")
340 user_id = self.get_secure_cookie("username")
324 if user_id == '' or (user_id is None and not self.application.password):
341 if user_id == '' or (user_id is None and not self.application.password):
325 user_id = 'anonymous'
342 user_id = 'anonymous'
326 return user_id
343 return user_id
327
344
328 def _inject_cookie_message(self, msg):
345 def _inject_cookie_message(self, msg):
329 """Inject the first message, which is the document cookie,
346 """Inject the first message, which is the document cookie,
330 for authentication."""
347 for authentication."""
331 if isinstance(msg, unicode):
348 if isinstance(msg, unicode):
332 # Cookie can't constructor doesn't accept unicode strings for some reason
349 # Cookie can't constructor doesn't accept unicode strings for some reason
333 msg = msg.encode('utf8', 'replace')
350 msg = msg.encode('utf8', 'replace')
334 try:
351 try:
335 self.request._cookies = Cookie.SimpleCookie(msg)
352 self.request._cookies = Cookie.SimpleCookie(msg)
336 except:
353 except:
337 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
354 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
338
355
339 def on_first_message(self, msg):
356 def on_first_message(self, msg):
340 self._inject_cookie_message(msg)
357 self._inject_cookie_message(msg)
341 if self.get_current_user() is None:
358 if self.get_current_user() is None:
342 logging.warn("Couldn't authenticate WebSocket connection")
359 logging.warn("Couldn't authenticate WebSocket connection")
343 raise web.HTTPError(403)
360 raise web.HTTPError(403)
344 self.on_message = self.save_on_message
361 self.on_message = self.save_on_message
345
362
346
363
347 class IOPubHandler(AuthenticatedZMQStreamHandler):
364 class IOPubHandler(AuthenticatedZMQStreamHandler):
348
365
349 def initialize(self, *args, **kwargs):
366 def initialize(self, *args, **kwargs):
350 self._kernel_alive = True
367 self._kernel_alive = True
351 self._beating = False
368 self._beating = False
352 self.iopub_stream = None
369 self.iopub_stream = None
353 self.hb_stream = None
370 self.hb_stream = None
354
371
355 def on_first_message(self, msg):
372 def on_first_message(self, msg):
356 try:
373 try:
357 super(IOPubHandler, self).on_first_message(msg)
374 super(IOPubHandler, self).on_first_message(msg)
358 except web.HTTPError:
375 except web.HTTPError:
359 self.close()
376 self.close()
360 return
377 return
361 km = self.application.kernel_manager
378 km = self.application.kernel_manager
362 self.time_to_dead = km.time_to_dead
379 self.time_to_dead = km.time_to_dead
363 kernel_id = self.kernel_id
380 kernel_id = self.kernel_id
364 try:
381 try:
365 self.iopub_stream = km.create_iopub_stream(kernel_id)
382 self.iopub_stream = km.create_iopub_stream(kernel_id)
366 self.hb_stream = km.create_hb_stream(kernel_id)
383 self.hb_stream = km.create_hb_stream(kernel_id)
367 except web.HTTPError:
384 except web.HTTPError:
368 # WebSockets don't response to traditional error codes so we
385 # WebSockets don't response to traditional error codes so we
369 # close the connection.
386 # close the connection.
370 if not self.stream.closed():
387 if not self.stream.closed():
371 self.stream.close()
388 self.stream.close()
372 self.close()
389 self.close()
373 else:
390 else:
374 self.iopub_stream.on_recv(self._on_zmq_reply)
391 self.iopub_stream.on_recv(self._on_zmq_reply)
375 self.start_hb(self.kernel_died)
392 self.start_hb(self.kernel_died)
376
393
377 def on_message(self, msg):
394 def on_message(self, msg):
378 pass
395 pass
379
396
380 def on_close(self):
397 def on_close(self):
381 # This method can be called twice, once by self.kernel_died and once
398 # This method can be called twice, once by self.kernel_died and once
382 # from the WebSocket close event. If the WebSocket connection is
399 # from the WebSocket close event. If the WebSocket connection is
383 # closed before the ZMQ streams are setup, they could be None.
400 # closed before the ZMQ streams are setup, they could be None.
384 self.stop_hb()
401 self.stop_hb()
385 if self.iopub_stream is not None and not self.iopub_stream.closed():
402 if self.iopub_stream is not None and not self.iopub_stream.closed():
386 self.iopub_stream.on_recv(None)
403 self.iopub_stream.on_recv(None)
387 self.iopub_stream.close()
404 self.iopub_stream.close()
388 if self.hb_stream is not None and not self.hb_stream.closed():
405 if self.hb_stream is not None and not self.hb_stream.closed():
389 self.hb_stream.close()
406 self.hb_stream.close()
390
407
391 def start_hb(self, callback):
408 def start_hb(self, callback):
392 """Start the heartbeating and call the callback if the kernel dies."""
409 """Start the heartbeating and call the callback if the kernel dies."""
393 if not self._beating:
410 if not self._beating:
394 self._kernel_alive = True
411 self._kernel_alive = True
395
412
396 def ping_or_dead():
413 def ping_or_dead():
397 if self._kernel_alive:
414 if self._kernel_alive:
398 self._kernel_alive = False
415 self._kernel_alive = False
399 self.hb_stream.send(b'ping')
416 self.hb_stream.send(b'ping')
400 else:
417 else:
401 try:
418 try:
402 callback()
419 callback()
403 except:
420 except:
404 pass
421 pass
405 finally:
422 finally:
406 self._hb_periodic_callback.stop()
423 self._hb_periodic_callback.stop()
407
424
408 def beat_received(msg):
425 def beat_received(msg):
409 self._kernel_alive = True
426 self._kernel_alive = True
410
427
411 self.hb_stream.on_recv(beat_received)
428 self.hb_stream.on_recv(beat_received)
412 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
429 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
413 self._hb_periodic_callback.start()
430 self._hb_periodic_callback.start()
414 self._beating= True
431 self._beating= True
415
432
416 def stop_hb(self):
433 def stop_hb(self):
417 """Stop the heartbeating and cancel all related callbacks."""
434 """Stop the heartbeating and cancel all related callbacks."""
418 if self._beating:
435 if self._beating:
419 self._hb_periodic_callback.stop()
436 self._hb_periodic_callback.stop()
420 if not self.hb_stream.closed():
437 if not self.hb_stream.closed():
421 self.hb_stream.on_recv(None)
438 self.hb_stream.on_recv(None)
422
439
423 def kernel_died(self):
440 def kernel_died(self):
424 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
441 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
425 self.write_message(
442 self.write_message(
426 {'header': {'msg_type': 'status'},
443 {'header': {'msg_type': 'status'},
427 'parent_header': {},
444 'parent_header': {},
428 'content': {'execution_state':'dead'}
445 'content': {'execution_state':'dead'}
429 }
446 }
430 )
447 )
431 self.on_close()
448 self.on_close()
432
449
433
450
434 class ShellHandler(AuthenticatedZMQStreamHandler):
451 class ShellHandler(AuthenticatedZMQStreamHandler):
435
452
436 def initialize(self, *args, **kwargs):
453 def initialize(self, *args, **kwargs):
437 self.shell_stream = None
454 self.shell_stream = None
438
455
439 def on_first_message(self, msg):
456 def on_first_message(self, msg):
440 try:
457 try:
441 super(ShellHandler, self).on_first_message(msg)
458 super(ShellHandler, self).on_first_message(msg)
442 except web.HTTPError:
459 except web.HTTPError:
443 self.close()
460 self.close()
444 return
461 return
445 km = self.application.kernel_manager
462 km = self.application.kernel_manager
446 self.max_msg_size = km.max_msg_size
463 self.max_msg_size = km.max_msg_size
447 kernel_id = self.kernel_id
464 kernel_id = self.kernel_id
448 try:
465 try:
449 self.shell_stream = km.create_shell_stream(kernel_id)
466 self.shell_stream = km.create_shell_stream(kernel_id)
450 except web.HTTPError:
467 except web.HTTPError:
451 # WebSockets don't response to traditional error codes so we
468 # WebSockets don't response to traditional error codes so we
452 # close the connection.
469 # close the connection.
453 if not self.stream.closed():
470 if not self.stream.closed():
454 self.stream.close()
471 self.stream.close()
455 self.close()
472 self.close()
456 else:
473 else:
457 self.shell_stream.on_recv(self._on_zmq_reply)
474 self.shell_stream.on_recv(self._on_zmq_reply)
458
475
459 def on_message(self, msg):
476 def on_message(self, msg):
460 if len(msg) < self.max_msg_size:
477 if len(msg) < self.max_msg_size:
461 msg = jsonapi.loads(msg)
478 msg = jsonapi.loads(msg)
462 self.session.send(self.shell_stream, msg)
479 self.session.send(self.shell_stream, msg)
463
480
464 def on_close(self):
481 def on_close(self):
465 # Make sure the stream exists and is not already closed.
482 # Make sure the stream exists and is not already closed.
466 if self.shell_stream is not None and not self.shell_stream.closed():
483 if self.shell_stream is not None and not self.shell_stream.closed():
467 self.shell_stream.close()
484 self.shell_stream.close()
468
485
469
486
470 #-----------------------------------------------------------------------------
487 #-----------------------------------------------------------------------------
471 # Notebook web service handlers
488 # Notebook web service handlers
472 #-----------------------------------------------------------------------------
489 #-----------------------------------------------------------------------------
473
490
474 class NotebookRootHandler(AuthenticatedHandler):
491 class NotebookRootHandler(AuthenticatedHandler):
475
492
476 @authenticate_unless_readonly
493 @authenticate_unless_readonly
477 def get(self):
494 def get(self):
478
495
479 nbm = self.application.notebook_manager
496 nbm = self.application.notebook_manager
480 files = nbm.list_notebooks()
497 files = nbm.list_notebooks()
481 self.finish(jsonapi.dumps(files))
498 self.finish(jsonapi.dumps(files))
482
499
483 @web.authenticated
500 @web.authenticated
484 def post(self):
501 def post(self):
485 nbm = self.application.notebook_manager
502 nbm = self.application.notebook_manager
486 body = self.request.body.strip()
503 body = self.request.body.strip()
487 format = self.get_argument('format', default='json')
504 format = self.get_argument('format', default='json')
488 name = self.get_argument('name', default=None)
505 name = self.get_argument('name', default=None)
489 if body:
506 if body:
490 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
507 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
491 else:
508 else:
492 notebook_id = nbm.new_notebook()
509 notebook_id = nbm.new_notebook()
493 self.set_header('Location', '/'+notebook_id)
510 self.set_header('Location', '/'+notebook_id)
494 self.finish(jsonapi.dumps(notebook_id))
511 self.finish(jsonapi.dumps(notebook_id))
495
512
496
513
497 class NotebookHandler(AuthenticatedHandler):
514 class NotebookHandler(AuthenticatedHandler):
498
515
499 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
516 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
500
517
501 @authenticate_unless_readonly
518 @authenticate_unless_readonly
502 def get(self, notebook_id):
519 def get(self, notebook_id):
503 nbm = self.application.notebook_manager
520 nbm = self.application.notebook_manager
504 format = self.get_argument('format', default='json')
521 format = self.get_argument('format', default='json')
505 last_mod, name, data = nbm.get_notebook(notebook_id, format)
522 last_mod, name, data = nbm.get_notebook(notebook_id, format)
506
523
507 if format == u'json':
524 if format == u'json':
508 self.set_header('Content-Type', 'application/json')
525 self.set_header('Content-Type', 'application/json')
509 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
526 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
510 elif format == u'py':
527 elif format == u'py':
511 self.set_header('Content-Type', 'application/x-python')
528 self.set_header('Content-Type', 'application/x-python')
512 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
529 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
513 self.set_header('Last-Modified', last_mod)
530 self.set_header('Last-Modified', last_mod)
514 self.finish(data)
531 self.finish(data)
515
532
516 @web.authenticated
533 @web.authenticated
517 def put(self, notebook_id):
534 def put(self, notebook_id):
518 nbm = self.application.notebook_manager
535 nbm = self.application.notebook_manager
519 format = self.get_argument('format', default='json')
536 format = self.get_argument('format', default='json')
520 name = self.get_argument('name', default=None)
537 name = self.get_argument('name', default=None)
521 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
538 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
522 self.set_status(204)
539 self.set_status(204)
523 self.finish()
540 self.finish()
524
541
525 @web.authenticated
542 @web.authenticated
526 def delete(self, notebook_id):
543 def delete(self, notebook_id):
527 nbm = self.application.notebook_manager
544 nbm = self.application.notebook_manager
528 nbm.delete_notebook(notebook_id)
545 nbm.delete_notebook(notebook_id)
529 self.set_status(204)
546 self.set_status(204)
530 self.finish()
547 self.finish()
531
548
532 #-----------------------------------------------------------------------------
549 #-----------------------------------------------------------------------------
533 # RST web service handlers
550 # RST web service handlers
534 #-----------------------------------------------------------------------------
551 #-----------------------------------------------------------------------------
535
552
536
553
537 class RSTHandler(AuthenticatedHandler):
554 class RSTHandler(AuthenticatedHandler):
538
555
539 @web.authenticated
556 @web.authenticated
540 def post(self):
557 def post(self):
541 if publish_string is None:
558 if publish_string is None:
542 raise web.HTTPError(503, u'docutils not available')
559 raise web.HTTPError(503, u'docutils not available')
543 body = self.request.body.strip()
560 body = self.request.body.strip()
544 source = body
561 source = body
545 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
562 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
546 defaults = {'file_insertion_enabled': 0,
563 defaults = {'file_insertion_enabled': 0,
547 'raw_enabled': 0,
564 'raw_enabled': 0,
548 '_disable_config': 1,
565 '_disable_config': 1,
549 'stylesheet_path': 0
566 'stylesheet_path': 0
550 # 'template': template_path
567 # 'template': template_path
551 }
568 }
552 try:
569 try:
553 html = publish_string(source, writer_name='html',
570 html = publish_string(source, writer_name='html',
554 settings_overrides=defaults
571 settings_overrides=defaults
555 )
572 )
556 except:
573 except:
557 raise web.HTTPError(400, u'Invalid RST')
574 raise web.HTTPError(400, u'Invalid RST')
558 print html
575 print html
559 self.set_header('Content-Type', 'text/html')
576 self.set_header('Content-Type', 'text/html')
560 self.finish(html)
577 self.finish(html)
561
578
562
579
@@ -1,332 +1,333 b''
1 """A tornado based IPython notebook server.
1 """A tornado based IPython notebook server.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import signal
23 import signal
24 import socket
24 import socket
25 import sys
25 import sys
26 import threading
26 import threading
27 import webbrowser
27 import webbrowser
28
28
29 # Third party
29 # Third party
30 import zmq
30 import zmq
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 import tornado.ioloop
35 import tornado.ioloop
36 tornado.ioloop.IOLoop = ioloop.IOLoop
36 tornado.ioloop.IOLoop = ioloop.IOLoop
37
37
38 from tornado import httpserver
38 from tornado import httpserver
39 from tornado import web
39 from tornado import web
40
40
41 # Our own libraries
41 # Our own libraries
42 from .kernelmanager import MappingKernelManager
42 from .kernelmanager import MappingKernelManager
43 from .handlers import (LoginHandler,
43 from .handlers import (LoginHandler, LogoutHandler,
44 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
44 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
45 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
45 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
46 ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
46 ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
47 )
47 )
48 from .notebookmanager import NotebookManager
48 from .notebookmanager import NotebookManager
49
49
50 from IPython.config.application import catch_config_error
50 from IPython.config.application import catch_config_error
51 from IPython.core.application import BaseIPythonApplication
51 from IPython.core.application import BaseIPythonApplication
52 from IPython.core.profiledir import ProfileDir
52 from IPython.core.profiledir import ProfileDir
53 from IPython.zmq.session import Session, default_secure
53 from IPython.zmq.session import Session, default_secure
54 from IPython.zmq.zmqshell import ZMQInteractiveShell
54 from IPython.zmq.zmqshell import ZMQInteractiveShell
55 from IPython.zmq.ipkernel import (
55 from IPython.zmq.ipkernel import (
56 flags as ipkernel_flags,
56 flags as ipkernel_flags,
57 aliases as ipkernel_aliases,
57 aliases as ipkernel_aliases,
58 IPKernelApp
58 IPKernelApp
59 )
59 )
60 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
60 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
61
61
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63 # Module globals
63 # Module globals
64 #-----------------------------------------------------------------------------
64 #-----------------------------------------------------------------------------
65
65
66 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
66 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
67 _kernel_action_regex = r"(?P<action>restart|interrupt)"
67 _kernel_action_regex = r"(?P<action>restart|interrupt)"
68 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
68 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
69
69
70 LOCALHOST = '127.0.0.1'
70 LOCALHOST = '127.0.0.1'
71
71
72 _examples = """
72 _examples = """
73 ipython notebook # start the notebook
73 ipython notebook # start the notebook
74 ipython notebook --profile=sympy # use the sympy profile
74 ipython notebook --profile=sympy # use the sympy profile
75 ipython notebook --pylab=inline # pylab in inline plotting mode
75 ipython notebook --pylab=inline # pylab in inline plotting mode
76 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
76 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
77 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
77 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
78 """
78 """
79
79
80 #-----------------------------------------------------------------------------
80 #-----------------------------------------------------------------------------
81 # The Tornado web application
81 # The Tornado web application
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83
83
84 class NotebookWebApplication(web.Application):
84 class NotebookWebApplication(web.Application):
85
85
86 def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
86 def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
87 handlers = [
87 handlers = [
88 (r"/", ProjectDashboardHandler),
88 (r"/", ProjectDashboardHandler),
89 (r"/login", LoginHandler),
89 (r"/login", LoginHandler),
90 (r"/logout", LogoutHandler),
90 (r"/new", NewHandler),
91 (r"/new", NewHandler),
91 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
92 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
92 (r"/kernels", MainKernelHandler),
93 (r"/kernels", MainKernelHandler),
93 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
94 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
94 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
95 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
95 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
96 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
96 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
97 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
97 (r"/notebooks", NotebookRootHandler),
98 (r"/notebooks", NotebookRootHandler),
98 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
99 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
99 (r"/rstservice/render", RSTHandler)
100 (r"/rstservice/render", RSTHandler)
100 ]
101 ]
101 settings = dict(
102 settings = dict(
102 template_path=os.path.join(os.path.dirname(__file__), "templates"),
103 template_path=os.path.join(os.path.dirname(__file__), "templates"),
103 static_path=os.path.join(os.path.dirname(__file__), "static"),
104 static_path=os.path.join(os.path.dirname(__file__), "static"),
104 cookie_secret=os.urandom(1024),
105 cookie_secret=os.urandom(1024),
105 login_url="/login",
106 login_url="/login",
106 )
107 )
107 web.Application.__init__(self, handlers, **settings)
108 web.Application.__init__(self, handlers, **settings)
108
109
109 self.kernel_manager = kernel_manager
110 self.kernel_manager = kernel_manager
110 self.log = log
111 self.log = log
111 self.notebook_manager = notebook_manager
112 self.notebook_manager = notebook_manager
112 self.ipython_app = ipython_app
113 self.ipython_app = ipython_app
113 self.read_only = self.ipython_app.read_only
114 self.read_only = self.ipython_app.read_only
114
115
115
116
116 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
117 # Aliases and Flags
118 # Aliases and Flags
118 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
119
120
120 flags = dict(ipkernel_flags)
121 flags = dict(ipkernel_flags)
121 flags['no-browser']=(
122 flags['no-browser']=(
122 {'NotebookApp' : {'open_browser' : False}},
123 {'NotebookApp' : {'open_browser' : False}},
123 "Don't open the notebook in a browser after startup."
124 "Don't open the notebook in a browser after startup."
124 )
125 )
125 flags['read-only'] = (
126 flags['read-only'] = (
126 {'NotebookApp' : {'read_only' : True}},
127 {'NotebookApp' : {'read_only' : True}},
127 """Allow read-only access to notebooks.
128 """Allow read-only access to notebooks.
128
129
129 When using a password to protect the notebook server, this flag
130 When using a password to protect the notebook server, this flag
130 allows unauthenticated clients to view the notebook list, and
131 allows unauthenticated clients to view the notebook list, and
131 individual notebooks, but not edit them, start kernels, or run
132 individual notebooks, but not edit them, start kernels, or run
132 code.
133 code.
133
134
134 If no password is set, the server will be entirely read-only.
135 If no password is set, the server will be entirely read-only.
135 """
136 """
136 )
137 )
137
138
138 # the flags that are specific to the frontend
139 # the flags that are specific to the frontend
139 # these must be scrubbed before being passed to the kernel,
140 # these must be scrubbed before being passed to the kernel,
140 # or it will raise an error on unrecognized flags
141 # or it will raise an error on unrecognized flags
141 notebook_flags = ['no-browser', 'read-only']
142 notebook_flags = ['no-browser', 'read-only']
142
143
143 aliases = dict(ipkernel_aliases)
144 aliases = dict(ipkernel_aliases)
144
145
145 aliases.update({
146 aliases.update({
146 'ip': 'NotebookApp.ip',
147 'ip': 'NotebookApp.ip',
147 'port': 'NotebookApp.port',
148 'port': 'NotebookApp.port',
148 'keyfile': 'NotebookApp.keyfile',
149 'keyfile': 'NotebookApp.keyfile',
149 'certfile': 'NotebookApp.certfile',
150 'certfile': 'NotebookApp.certfile',
150 'notebook-dir': 'NotebookManager.notebook_dir',
151 'notebook-dir': 'NotebookManager.notebook_dir',
151 })
152 })
152
153
153 # remove ipkernel flags that are singletons, and don't make sense in
154 # remove ipkernel flags that are singletons, and don't make sense in
154 # multi-kernel evironment:
155 # multi-kernel evironment:
155 aliases.pop('f', None)
156 aliases.pop('f', None)
156
157
157 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
158 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
158 u'notebook-dir']
159 u'notebook-dir']
159
160
160 #-----------------------------------------------------------------------------
161 #-----------------------------------------------------------------------------
161 # NotebookApp
162 # NotebookApp
162 #-----------------------------------------------------------------------------
163 #-----------------------------------------------------------------------------
163
164
164 class NotebookApp(BaseIPythonApplication):
165 class NotebookApp(BaseIPythonApplication):
165
166
166 name = 'ipython-notebook'
167 name = 'ipython-notebook'
167 default_config_file_name='ipython_notebook_config.py'
168 default_config_file_name='ipython_notebook_config.py'
168
169
169 description = """
170 description = """
170 The IPython HTML Notebook.
171 The IPython HTML Notebook.
171
172
172 This launches a Tornado based HTML Notebook Server that serves up an
173 This launches a Tornado based HTML Notebook Server that serves up an
173 HTML5/Javascript Notebook client.
174 HTML5/Javascript Notebook client.
174 """
175 """
175 examples = _examples
176 examples = _examples
176
177
177 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
178 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
178 MappingKernelManager, NotebookManager]
179 MappingKernelManager, NotebookManager]
179 flags = Dict(flags)
180 flags = Dict(flags)
180 aliases = Dict(aliases)
181 aliases = Dict(aliases)
181
182
182 kernel_argv = List(Unicode)
183 kernel_argv = List(Unicode)
183
184
184 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
185 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
185 default_value=logging.INFO,
186 default_value=logging.INFO,
186 config=True,
187 config=True,
187 help="Set the log level by value or name.")
188 help="Set the log level by value or name.")
188
189
189 # Network related information.
190 # Network related information.
190
191
191 ip = Unicode(LOCALHOST, config=True,
192 ip = Unicode(LOCALHOST, config=True,
192 help="The IP address the notebook server will listen on."
193 help="The IP address the notebook server will listen on."
193 )
194 )
194
195
195 def _ip_changed(self, name, old, new):
196 def _ip_changed(self, name, old, new):
196 if new == u'*': self.ip = u''
197 if new == u'*': self.ip = u''
197
198
198 port = Integer(8888, config=True,
199 port = Integer(8888, config=True,
199 help="The port the notebook server will listen on."
200 help="The port the notebook server will listen on."
200 )
201 )
201
202
202 certfile = Unicode(u'', config=True,
203 certfile = Unicode(u'', config=True,
203 help="""The full path to an SSL/TLS certificate file."""
204 help="""The full path to an SSL/TLS certificate file."""
204 )
205 )
205
206
206 keyfile = Unicode(u'', config=True,
207 keyfile = Unicode(u'', config=True,
207 help="""The full path to a private key file for usage with SSL/TLS."""
208 help="""The full path to a private key file for usage with SSL/TLS."""
208 )
209 )
209
210
210 password = Unicode(u'', config=True,
211 password = Unicode(u'', config=True,
211 help="""Hashed password to use for web authentication.
212 help="""Hashed password to use for web authentication.
212
213
213 To generate, type in a python/IPython shell:
214 To generate, type in a python/IPython shell:
214
215
215 from IPython.lib import passwd; passwd()
216 from IPython.lib import passwd; passwd()
216
217
217 The string should be of the form type:salt:hashed-password.
218 The string should be of the form type:salt:hashed-password.
218 """
219 """
219 )
220 )
220
221
221 open_browser = Bool(True, config=True,
222 open_browser = Bool(True, config=True,
222 help="Whether to open in a browser after starting.")
223 help="Whether to open in a browser after starting.")
223
224
224 read_only = Bool(False, config=True,
225 read_only = Bool(False, config=True,
225 help="Whether to prevent editing/execution of notebooks."
226 help="Whether to prevent editing/execution of notebooks."
226 )
227 )
227
228
228 def parse_command_line(self, argv=None):
229 def parse_command_line(self, argv=None):
229 super(NotebookApp, self).parse_command_line(argv)
230 super(NotebookApp, self).parse_command_line(argv)
230 if argv is None:
231 if argv is None:
231 argv = sys.argv[1:]
232 argv = sys.argv[1:]
232
233
233 self.kernel_argv = list(argv) # copy
234 self.kernel_argv = list(argv) # copy
234 # Kernel should inherit default config file from frontend
235 # Kernel should inherit default config file from frontend
235 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
236 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
236 # Scrub frontend-specific flags
237 # Scrub frontend-specific flags
237 for a in argv:
238 for a in argv:
238 if a.startswith('-') and a.lstrip('-') in notebook_flags:
239 if a.startswith('-') and a.lstrip('-') in notebook_flags:
239 self.kernel_argv.remove(a)
240 self.kernel_argv.remove(a)
240 swallow_next = False
241 swallow_next = False
241 for a in argv:
242 for a in argv:
242 if swallow_next:
243 if swallow_next:
243 self.kernel_argv.remove(a)
244 self.kernel_argv.remove(a)
244 swallow_next = False
245 swallow_next = False
245 continue
246 continue
246 if a.startswith('-'):
247 if a.startswith('-'):
247 split = a.lstrip('-').split('=')
248 split = a.lstrip('-').split('=')
248 alias = split[0]
249 alias = split[0]
249 if alias in notebook_aliases:
250 if alias in notebook_aliases:
250 self.kernel_argv.remove(a)
251 self.kernel_argv.remove(a)
251 if len(split) == 1:
252 if len(split) == 1:
252 # alias passed with arg via space
253 # alias passed with arg via space
253 swallow_next = True
254 swallow_next = True
254
255
255 def init_configurables(self):
256 def init_configurables(self):
256 # Don't let Qt or ZMQ swallow KeyboardInterupts.
257 # Don't let Qt or ZMQ swallow KeyboardInterupts.
257 signal.signal(signal.SIGINT, signal.SIG_DFL)
258 signal.signal(signal.SIGINT, signal.SIG_DFL)
258
259
259 # force Session default to be secure
260 # force Session default to be secure
260 default_secure(self.config)
261 default_secure(self.config)
261 # Create a KernelManager and start a kernel.
262 # Create a KernelManager and start a kernel.
262 self.kernel_manager = MappingKernelManager(
263 self.kernel_manager = MappingKernelManager(
263 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
264 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
264 connection_dir = self.profile_dir.security_dir,
265 connection_dir = self.profile_dir.security_dir,
265 )
266 )
266 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
267 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
267 self.notebook_manager.list_notebooks()
268 self.notebook_manager.list_notebooks()
268
269
269 def init_logging(self):
270 def init_logging(self):
270 super(NotebookApp, self).init_logging()
271 super(NotebookApp, self).init_logging()
271 # This prevents double log messages because tornado use a root logger that
272 # This prevents double log messages because tornado use a root logger that
272 # self.log is a child of. The logging module dipatches log messages to a log
273 # self.log is a child of. The logging module dipatches log messages to a log
273 # and all of its ancenstors until propagate is set to False.
274 # and all of its ancenstors until propagate is set to False.
274 self.log.propagate = False
275 self.log.propagate = False
275
276
276 @catch_config_error
277 @catch_config_error
277 def initialize(self, argv=None):
278 def initialize(self, argv=None):
278 super(NotebookApp, self).initialize(argv)
279 super(NotebookApp, self).initialize(argv)
279 self.init_configurables()
280 self.init_configurables()
280 self.web_app = NotebookWebApplication(
281 self.web_app = NotebookWebApplication(
281 self, self.kernel_manager, self.notebook_manager, self.log
282 self, self.kernel_manager, self.notebook_manager, self.log
282 )
283 )
283 if self.certfile:
284 if self.certfile:
284 ssl_options = dict(certfile=self.certfile)
285 ssl_options = dict(certfile=self.certfile)
285 if self.keyfile:
286 if self.keyfile:
286 ssl_options['keyfile'] = self.keyfile
287 ssl_options['keyfile'] = self.keyfile
287 else:
288 else:
288 ssl_options = None
289 ssl_options = None
289 self.web_app.password = self.password
290 self.web_app.password = self.password
290 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
291 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
291 if ssl_options is None and not self.ip:
292 if ssl_options is None and not self.ip:
292 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
293 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
293 'but not using any encryption or authentication. This is highly '
294 'but not using any encryption or authentication. This is highly '
294 'insecure and not recommended.')
295 'insecure and not recommended.')
295
296
296 # Try random ports centered around the default.
297 # Try random ports centered around the default.
297 from random import randint
298 from random import randint
298 n = 50 # Max number of attempts, keep reasonably large.
299 n = 50 # Max number of attempts, keep reasonably large.
299 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
300 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
300 try:
301 try:
301 self.http_server.listen(port, self.ip)
302 self.http_server.listen(port, self.ip)
302 except socket.error, e:
303 except socket.error, e:
303 if e.errno != errno.EADDRINUSE:
304 if e.errno != errno.EADDRINUSE:
304 raise
305 raise
305 self.log.info('The port %i is already in use, trying another random port.' % port)
306 self.log.info('The port %i is already in use, trying another random port.' % port)
306 else:
307 else:
307 self.port = port
308 self.port = port
308 break
309 break
309
310
310 def start(self):
311 def start(self):
311 ip = self.ip if self.ip else '[all ip addresses on your system]'
312 ip = self.ip if self.ip else '[all ip addresses on your system]'
312 proto = 'https' if self.certfile else 'http'
313 proto = 'https' if self.certfile else 'http'
313 self.log.info("The IPython Notebook is running at: %s://%s:%i" % (proto,
314 self.log.info("The IPython Notebook is running at: %s://%s:%i" % (proto,
314 ip,
315 ip,
315 self.port))
316 self.port))
316 if self.open_browser:
317 if self.open_browser:
317 ip = self.ip or '127.0.0.1'
318 ip = self.ip or '127.0.0.1'
318 b = lambda : webbrowser.open("%s://%s:%i" % (proto, ip, self.port),
319 b = lambda : webbrowser.open("%s://%s:%i" % (proto, ip, self.port),
319 new=2)
320 new=2)
320 threading.Thread(target=b).start()
321 threading.Thread(target=b).start()
321
322
322 ioloop.IOLoop.instance().start()
323 ioloop.IOLoop.instance().start()
323
324
324 #-----------------------------------------------------------------------------
325 #-----------------------------------------------------------------------------
325 # Main entry point
326 # Main entry point
326 #-----------------------------------------------------------------------------
327 #-----------------------------------------------------------------------------
327
328
328 def launch_new_instance():
329 def launch_new_instance():
329 app = NotebookApp()
330 app = NotebookApp()
330 app.initialize()
331 app.initialize()
331 app.start()
332 app.start()
332
333
@@ -1,115 +1,130 b''
1
1
2 .border-box-sizing {
2 .border-box-sizing {
3 box-sizing: border-box;
3 box-sizing: border-box;
4 -moz-box-sizing: border-box;
4 -moz-box-sizing: border-box;
5 -webkit-box-sizing: border-box;
5 -webkit-box-sizing: border-box;
6 }
6 }
7
7
8 /* Flexible box model classes */
8 /* Flexible box model classes */
9 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
9 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
10
10
11 .hbox {
11 .hbox {
12 display: -webkit-box;
12 display: -webkit-box;
13 -webkit-box-orient: horizontal;
13 -webkit-box-orient: horizontal;
14 -webkit-box-align: stretch;
14 -webkit-box-align: stretch;
15
15
16 display: -moz-box;
16 display: -moz-box;
17 -moz-box-orient: horizontal;
17 -moz-box-orient: horizontal;
18 -moz-box-align: stretch;
18 -moz-box-align: stretch;
19
19
20 display: box;
20 display: box;
21 box-orient: horizontal;
21 box-orient: horizontal;
22 box-align: stretch;
22 box-align: stretch;
23 }
23 }
24
24
25 .hbox > * {
25 .hbox > * {
26 -webkit-box-flex: 0;
26 -webkit-box-flex: 0;
27 -moz-box-flex: 0;
27 -moz-box-flex: 0;
28 box-flex: 0;
28 box-flex: 0;
29 }
29 }
30
30
31 .vbox {
31 .vbox {
32 display: -webkit-box;
32 display: -webkit-box;
33 -webkit-box-orient: vertical;
33 -webkit-box-orient: vertical;
34 -webkit-box-align: stretch;
34 -webkit-box-align: stretch;
35
35
36 display: -moz-box;
36 display: -moz-box;
37 -moz-box-orient: vertical;
37 -moz-box-orient: vertical;
38 -moz-box-align: stretch;
38 -moz-box-align: stretch;
39
39
40 display: box;
40 display: box;
41 box-orient: vertical;
41 box-orient: vertical;
42 box-align: stretch;
42 box-align: stretch;
43 }
43 }
44
44
45 .vbox > * {
45 .vbox > * {
46 -webkit-box-flex: 0;
46 -webkit-box-flex: 0;
47 -moz-box-flex: 0;
47 -moz-box-flex: 0;
48 box-flex: 0;
48 box-flex: 0;
49 }
49 }
50
50
51 .reverse {
51 .reverse {
52 -webkit-box-direction: reverse;
52 -webkit-box-direction: reverse;
53 -moz-box-direction: reverse;
53 -moz-box-direction: reverse;
54 box-direction: reverse;
54 box-direction: reverse;
55 }
55 }
56
56
57 .box-flex0 {
57 .box-flex0 {
58 -webkit-box-flex: 0;
58 -webkit-box-flex: 0;
59 -moz-box-flex: 0;
59 -moz-box-flex: 0;
60 box-flex: 0;
60 box-flex: 0;
61 }
61 }
62
62
63 .box-flex1, .box-flex {
63 .box-flex1, .box-flex {
64 -webkit-box-flex: 1;
64 -webkit-box-flex: 1;
65 -moz-box-flex: 1;
65 -moz-box-flex: 1;
66 box-flex: 1;
66 box-flex: 1;
67 }
67 }
68
68
69 .box-flex2 {
69 .box-flex2 {
70 -webkit-box-flex: 2;
70 -webkit-box-flex: 2;
71 -moz-box-flex: 2;
71 -moz-box-flex: 2;
72 box-flex: 2;
72 box-flex: 2;
73 }
73 }
74
74
75 .box-group1 {
75 .box-group1 {
76 -webkit-box-flex-group: 1;
76 -webkit-box-flex-group: 1;
77 -moz-box-flex-group: 1;
77 -moz-box-flex-group: 1;
78 box-flex-group: 1;
78 box-flex-group: 1;
79 }
79 }
80
80
81 .box-group2 {
81 .box-group2 {
82 -webkit-box-flex-group: 2;
82 -webkit-box-flex-group: 2;
83 -moz-box-flex-group: 2;
83 -moz-box-flex-group: 2;
84 box-flex-group: 2;
84 box-flex-group: 2;
85 }
85 }
86
86
87 .start {
87 .start {
88 -webkit-box-pack: start;
88 -webkit-box-pack: start;
89 -moz-box-pack: start;
89 -moz-box-pack: start;
90 box-pack: start;
90 box-pack: start;
91 }
91 }
92
92
93 .end {
93 .end {
94 -webkit-box-pack: end;
94 -webkit-box-pack: end;
95 -moz-box-pack: end;
95 -moz-box-pack: end;
96 box-pack: end;
96 box-pack: end;
97 }
97 }
98
98
99 .center {
99 .center {
100 -webkit-box-pack: center;
100 -webkit-box-pack: center;
101 -moz-box-pack: center;
101 -moz-box-pack: center;
102 box-pack: center;
102 box-pack: center;
103 }
103 }
104
104
105 #message {
105 .message {
106 border: 1px solid red;
106 border-width: 1px;
107 background-color: #FFD3D1;
107 border-style: solid;
108 text-align: center;
108 text-align: center;
109 padding: 0.5em;
109 padding: 0.5em;
110 margin: 0.5em;
110 margin: 0.5em 0;
111 }
112
113 .message.error {
114 background-color: #FFD3D1;
115 border-color: red;
116 }
117
118 .message.warning {
119 background-color: #FFD09E;
120 border-color: orange;
121 }
122
123 .message.info {
124 background-color: #CBFFBA;
125 border-color: green;
111 }
126 }
112
127
113 #content_panel {
128 #content_panel {
114 margin: 0.5em;
129 margin: 0.5em;
115 } No newline at end of file
130 }
@@ -1,38 +1,38 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Login button
9 // Login button
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
13
14 var LoginWidget = function (selector) {
14 var LoginWidget = function (selector) {
15 this.selector = selector;
15 this.selector = selector;
16 if (this.selector !== undefined) {
16 if (this.selector !== undefined) {
17 this.element = $(selector);
17 this.element = $(selector);
18 this.style();
18 this.style();
19 this.bind_events();
19 this.bind_events();
20 }
20 }
21 };
21 };
22
22
23 LoginWidget.prototype.style = function () {
23 LoginWidget.prototype.style = function () {
24 this.element.find('button#login').button();
24 this.element.find('button#logout').button();
25 };
25 };
26 LoginWidget.prototype.bind_events = function () {
26 LoginWidget.prototype.bind_events = function () {
27 var that = this;
27 var that = this;
28 this.element.find("button#login").click(function () {
28 this.element.find("button#logout").click(function () {
29 window.location = "/login?next="+location.pathname;
29 window.location = "/logout";
30 });
30 });
31 };
31 };
32
32
33 // Set module variables
33 // Set module variables
34 IPython.LoginWidget = LoginWidget;
34 IPython.LoginWidget = LoginWidget;
35
35
36 return IPython;
36 return IPython;
37
37
38 }(IPython));
38 }(IPython));
@@ -1,61 +1,8 b''
1 <!DOCTYPE HTML>
1 {% extends layout.html %}
2 <html>
2
3
3 {% block content_panel %}
4 <head>
4 <form action="/login?next={{url_escape(next)}}" method="post">
5 <meta charset="utf-8">
5 Password: <input type="password" name="password">
6
6 <input type="submit" value="Sign in" id="signin">
7 <title>IPython Notebook</title>
7 </form>
8
8 {% end %}
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
13
14 <meta name="read_only" content="{{read_only}}"/>
15
16 </head>
17
18 <body>
19
20 <div id="header">
21 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
22 </div>
23
24 <div id="header_border"></div>
25
26 <div id="main_app">
27
28 <div id="app_hbox">
29
30 <div id="left_panel">
31 </div>
32
33 <div id="content_panel">
34 {% if message %}
35 <div id="message">
36 {{message}}
37 </div>
38 {% end %}
39
40 <form action="/login?next={{url_escape(next)}}" method="post">
41 Password: <input type="password" name="password">
42 <input type="submit" value="Sign in" id="signin">
43 </form>
44 </div>
45 <div id="right_panel">
46 </div>
47
48 </div>
49
50 </div>
51
52 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
53 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
54 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
55 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
56
57 </body>
58
59 </html>
60
61
@@ -1,295 +1,301 b''
1 <!DOCTYPE HTML>
1 <!DOCTYPE HTML>
2 <html>
2 <html>
3
3
4 <head>
4 <head>
5 <meta charset="utf-8">
5 <meta charset="utf-8">
6
6
7 <title>IPython Notebook</title>
7 <title>IPython Notebook</title>
8
8
9 <!-- <script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" charset="utf-8"></script> -->
9 <!-- <script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" charset="utf-8"></script> -->
10 <script type='text/javascript' src='static/mathjax/MathJax.js?config=TeX-AMS_HTML' charset='utf-8'></script>
10 <script type='text/javascript' src='static/mathjax/MathJax.js?config=TeX-AMS_HTML' charset='utf-8'></script>
11 <script type="text/javascript">
11 <script type="text/javascript">
12 function CheckMathJax(){
12 function CheckMathJax(){
13 var div=document.getElementById("MathJaxFetchingWarning")
13 var div=document.getElementById("MathJaxFetchingWarning")
14 if(window.MathJax){
14 if(window.MathJax){
15 document.body.removeChild(div)
15 document.body.removeChild(div)
16 }
16 }
17 else{
17 else{
18 div.style.display = "block";
18 div.style.display = "block";
19 }
19 }
20 }
20 }
21 if (typeof MathJax == 'undefined') {
21 if (typeof MathJax == 'undefined') {
22 console.log("No local MathJax, loading from CDN");
22 console.log("No local MathJax, loading from CDN");
23 document.write(unescape("%3Cscript type='text/javascript' src='http://cdn.mathjax.org/mathjax/latest/MathJax.js%3Fconfig=TeX-AMS_HTML' charset='utf-8'%3E%3C/script%3E"));
23 document.write(unescape("%3Cscript type='text/javascript' src='http://cdn.mathjax.org/mathjax/latest/MathJax.js%3Fconfig=TeX-AMS_HTML' charset='utf-8'%3E%3C/script%3E"));
24 }else{
24 }else{
25 console.log("Using local MathJax");
25 console.log("Using local MathJax");
26 }
26 }
27 </script>
27 </script>
28
28
29 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
29 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
30 <link rel="stylesheet" href="static/codemirror/lib/codemirror.css">
30 <link rel="stylesheet" href="static/codemirror/lib/codemirror.css">
31 <link rel="stylesheet" href="static/codemirror/mode/markdown/markdown.css">
31 <link rel="stylesheet" href="static/codemirror/mode/markdown/markdown.css">
32 <link rel="stylesheet" href="static/codemirror/mode/rst/rst.css">
32 <link rel="stylesheet" href="static/codemirror/mode/rst/rst.css">
33 <link rel="stylesheet" href="static/codemirror/theme/ipython.css">
33 <link rel="stylesheet" href="static/codemirror/theme/ipython.css">
34 <link rel="stylesheet" href="static/codemirror/theme/default.css">
34 <link rel="stylesheet" href="static/codemirror/theme/default.css">
35
35
36 <link rel="stylesheet" href="static/prettify/prettify.css"/>
36 <link rel="stylesheet" href="static/prettify/prettify.css"/>
37
37
38 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
38 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
39 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
39 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
40 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
40 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
41 <link rel="stylesheet" href="static/css/notebook.css" type="text/css" />
41 <link rel="stylesheet" href="static/css/notebook.css" type="text/css" />
42 <link rel="stylesheet" href="static/css/renderedhtml.css" type="text/css" />
42 <link rel="stylesheet" href="static/css/renderedhtml.css" type="text/css" />
43
43
44 <meta name="read_only" content="{{read_only}}"/>
44 <meta name="read_only" content="{{read_only}}"/>
45
45
46 </head>
46 </head>
47
47
48 <body onload='CheckMathJax();'
48 <body onload='CheckMathJax();'
49 data-project={{project}} data-notebook-id={{notebook_id}}
49 data-project={{project}} data-notebook-id={{notebook_id}}
50 data-base-project-url={{base_project_url}} data-base-kernel-url={{base_kernel_url}}
50 data-base-project-url={{base_project_url}} data-base-kernel-url={{base_kernel_url}}
51 >
51 >
52
52
53 <div id="header">
53 <div id="header">
54 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
54 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
55 <span id="save_widget">
55 <span id="save_widget">
56 <input type="text" id="notebook_name" size="20"></textarea>
56 <input type="text" id="notebook_name" size="20"></textarea>
57 <button id="save_notebook"><u>S</u>ave</button>
57 <button id="save_notebook"><u>S</u>ave</button>
58 </span>
58 </span>
59 <span id="quick_help_area">
59 <span id="quick_help_area">
60 <button id="quick_help">Quick<u>H</u>elp</button>
60 <button id="quick_help">Quick<u>H</u>elp</button>
61 </span>
61 </span>
62 <span id="login_widget" class="hidden">
62
63 <button id="login">Login</button>
63 <span id="login_widget">
64 {% comment This is a temporary workaround to hide the logout button %}
65 {% comment when appropriate until notebook.html is templated %}
66 {% if current_user and current_user != 'anonymous' %}
67 <button id="logout">Logout</button>
68 {% end %}
64 </span>
69 </span>
70
65 <span id="kernel_status">Idle</span>
71 <span id="kernel_status">Idle</span>
66 </div>
72 </div>
67
73
68 <div id="MathJaxFetchingWarning"
74 <div id="MathJaxFetchingWarning"
69 style="width:80%; margin:auto;padding-top:20%;text-align: justify; display:none">
75 style="width:80%; margin:auto;padding-top:20%;text-align: justify; display:none">
70 <p style="font-size:26px;">There was an issue trying to fetch MathJax.js
76 <p style="font-size:26px;">There was an issue trying to fetch MathJax.js
71 from the internet.</p>
77 from the internet.</p>
72
78
73 <p style="padding:0.2em"> With a working internet connection, you can run
79 <p style="padding:0.2em"> With a working internet connection, you can run
74 the following at a Python or IPython prompt, which will install a local
80 the following at a Python or IPython prompt, which will install a local
75 copy of MathJax:</p>
81 copy of MathJax:</p>
76
82
77 <pre style="background-color:lightblue;border:thin silver solid;padding:0.4em">
83 <pre style="background-color:lightblue;border:thin silver solid;padding:0.4em">
78 from IPython.external import mathjax; mathjax.install_mathjax()
84 from IPython.external import mathjax; mathjax.install_mathjax()
79 </pre>
85 </pre>
80 This will try to install MathJax into the directory where you installed
86 This will try to install MathJax into the directory where you installed
81 IPython. If you installed IPython to a location that requires
87 IPython. If you installed IPython to a location that requires
82 administrative privileges to write, you will need to make this call as
88 administrative privileges to write, you will need to make this call as
83 an administrator. On OSX/Linux/Unix, this can be done at the
89 an administrator. On OSX/Linux/Unix, this can be done at the
84 command-line via:
90 command-line via:
85 <pre style="background-color:lightblue;border:thin silver solid;padding:0.4em">
91 <pre style="background-color:lightblue;border:thin silver solid;padding:0.4em">
86 sudo python -c "from IPython.external import mathjax; mathjax.install_mathjax()"
92 sudo python -c "from IPython.external import mathjax; mathjax.install_mathjax()"
87 </pre>
93 </pre>
88 </p>
94 </p>
89 </div>
95 </div>
90
96
91 <div id="main_app">
97 <div id="main_app">
92
98
93 <div id="left_panel">
99 <div id="left_panel">
94
100
95 <div id="notebook_section">
101 <div id="notebook_section">
96 <div class="section_header">
102 <div class="section_header">
97 <h3>Notebook</h3>
103 <h3>Notebook</h3>
98 </div>
104 </div>
99 <div class="section_content">
105 <div class="section_content">
100 <div class="section_row">
106 <div class="section_row">
101 <span id="new_open" class="section_row_buttons">
107 <span id="new_open" class="section_row_buttons">
102 <button id="new_notebook">New</button>
108 <button id="new_notebook">New</button>
103 <button id="open_notebook">Open</button>
109 <button id="open_notebook">Open</button>
104 </span>
110 </span>
105 <span class="section_row_header">Actions</span>
111 <span class="section_row_header">Actions</span>
106 </div>
112 </div>
107 <div class="section_row">
113 <div class="section_row">
108 <span>
114 <span>
109 <select id="download_format">
115 <select id="download_format">
110 <option value="json">ipynb</option>
116 <option value="json">ipynb</option>
111 <option value="py">py</option>
117 <option value="py">py</option>
112 </select>
118 </select>
113 </span>
119 </span>
114 <span class="section_row_buttons">
120 <span class="section_row_buttons">
115 <button id="download_notebook">Download</button>
121 <button id="download_notebook">Download</button>
116 </span>
122 </span>
117 </div>
123 </div>
118 <div class="section_row">
124 <div class="section_row">
119 <span class="section_row_buttons">
125 <span class="section_row_buttons">
120 <span id="print_widget">
126 <span id="print_widget">
121 <button id="print_notebook">Print</button>
127 <button id="print_notebook">Print</button>
122 </span>
128 </span>
123 </span>
129 </span>
124 </div>
130 </div>
125 </div>
131 </div>
126 </div>
132 </div>
127
133
128 <div id="cell_section">
134 <div id="cell_section">
129 <div class="section_header">
135 <div class="section_header">
130 <h3>Cell</h3>
136 <h3>Cell</h3>
131 </div>
137 </div>
132 <div class="section_content">
138 <div class="section_content">
133 <div class="section_row">
139 <div class="section_row">
134 <span class="section_row_buttons">
140 <span class="section_row_buttons">
135 <button id="delete_cell"><u>D</u>elete</button>
141 <button id="delete_cell"><u>D</u>elete</button>
136 </span>
142 </span>
137 <span class="section_row_header">Actions</span>
143 <span class="section_row_header">Actions</span>
138 </div>
144 </div>
139 <div class="section_row">
145 <div class="section_row">
140 <span id="cell_type" class="section_row_buttons">
146 <span id="cell_type" class="section_row_buttons">
141 <button id="to_code"><u>C</u>ode</button>
147 <button id="to_code"><u>C</u>ode</button>
142 <!-- <button id="to_html">HTML</button>-->
148 <!-- <button id="to_html">HTML</button>-->
143 <button id="to_markdown"><u>M</u>arkdown</button>
149 <button id="to_markdown"><u>M</u>arkdown</button>
144 </span>
150 </span>
145 <span class="button_label">Format</span>
151 <span class="button_label">Format</span>
146 </div>
152 </div>
147 <div class="section_row">
153 <div class="section_row">
148 <span id="cell_output" class="section_row_buttons">
154 <span id="cell_output" class="section_row_buttons">
149 <button id="toggle_output"><u>T</u>oggle</button>
155 <button id="toggle_output"><u>T</u>oggle</button>
150 <button id="clear_all_output">ClearAll</button>
156 <button id="clear_all_output">ClearAll</button>
151 </span>
157 </span>
152 <span class="button_label">Output</span>
158 <span class="button_label">Output</span>
153 </div>
159 </div>
154 <div class="section_row">
160 <div class="section_row">
155 <span id="insert" class="section_row_buttons">
161 <span id="insert" class="section_row_buttons">
156 <button id="insert_cell_above"><u>A</u>bove</button>
162 <button id="insert_cell_above"><u>A</u>bove</button>
157 <button id="insert_cell_below"><u>B</u>elow</button>
163 <button id="insert_cell_below"><u>B</u>elow</button>
158 </span>
164 </span>
159 <span class="button_label">Insert</span>
165 <span class="button_label">Insert</span>
160 </div>
166 </div>
161 <div class="section_row">
167 <div class="section_row">
162 <span id="move" class="section_row_buttons">
168 <span id="move" class="section_row_buttons">
163 <button id="move_cell_up">Up</button>
169 <button id="move_cell_up">Up</button>
164 <button id="move_cell_down">Down</button>
170 <button id="move_cell_down">Down</button>
165 </span>
171 </span>
166 <span class="button_label">Move</span>
172 <span class="button_label">Move</span>
167 </div>
173 </div>
168 <div class="section_row">
174 <div class="section_row">
169 <span id="run_cells" class="section_row_buttons">
175 <span id="run_cells" class="section_row_buttons">
170 <button id="run_selected_cell">Selected</button>
176 <button id="run_selected_cell">Selected</button>
171 <button id="run_all_cells">All</button>
177 <button id="run_all_cells">All</button>
172 </span>
178 </span>
173 <span class="button_label">Run</span>
179 <span class="button_label">Run</span>
174 </div>
180 </div>
175 <div class="section_row">
181 <div class="section_row">
176 <span id="autoindent_span">
182 <span id="autoindent_span">
177 <input type="checkbox" id="autoindent" checked="true"></input>
183 <input type="checkbox" id="autoindent" checked="true"></input>
178 </span>
184 </span>
179 <span class="checkbox_label" id="autoindent_label">Autoindent:</span>
185 <span class="checkbox_label" id="autoindent_label">Autoindent:</span>
180 </div>
186 </div>
181 </div>
187 </div>
182 </div>
188 </div>
183
189
184 <div id="kernel_section">
190 <div id="kernel_section">
185 <div class="section_header">
191 <div class="section_header">
186 <h3>Kernel</h3>
192 <h3>Kernel</h3>
187 </div>
193 </div>
188 <div class="section_content">
194 <div class="section_content">
189 <div class="section_row">
195 <div class="section_row">
190 <span id="int_restart" class="section_row_buttons">
196 <span id="int_restart" class="section_row_buttons">
191 <button id="int_kernel"><u>I</u>nterrupt</button>
197 <button id="int_kernel"><u>I</u>nterrupt</button>
192 <button id="restart_kernel">Restart</button>
198 <button id="restart_kernel">Restart</button>
193 </span>
199 </span>
194 <span class="section_row_header">Actions</span>
200 <span class="section_row_header">Actions</span>
195 </div>
201 </div>
196 <div class="section_row">
202 <div class="section_row">
197 <span id="kernel_persist">
203 <span id="kernel_persist">
198 {% if kill_kernel %}
204 {% if kill_kernel %}
199 <input type="checkbox" id="kill_kernel" checked="true"></input>
205 <input type="checkbox" id="kill_kernel" checked="true"></input>
200 {% else %}
206 {% else %}
201 <input type="checkbox" id="kill_kernel"></input>
207 <input type="checkbox" id="kill_kernel"></input>
202 {% end %}
208 {% end %}
203 </span>
209 </span>
204 <span class="checkbox_label" id="kill_kernel_label">Kill kernel upon exit:</span>
210 <span class="checkbox_label" id="kill_kernel_label">Kill kernel upon exit:</span>
205 </div>
211 </div>
206 </div>
212 </div>
207 </div>
213 </div>
208
214
209 <div id="help_section">
215 <div id="help_section">
210 <div class="section_header">
216 <div class="section_header">
211 <h3>Help</h3>
217 <h3>Help</h3>
212 </div>
218 </div>
213 <div class="section_content">
219 <div class="section_content">
214 <div class="section_row">
220 <div class="section_row">
215 <span id="help_buttons0" class="section_row_buttons">
221 <span id="help_buttons0" class="section_row_buttons">
216 <a id="python_help" href="http://docs.python.org" target="_blank">Python</a>
222 <a id="python_help" href="http://docs.python.org" target="_blank">Python</a>
217 <a id="ipython_help" href="http://ipython.org/documentation.html" target="_blank">IPython</a>
223 <a id="ipython_help" href="http://ipython.org/documentation.html" target="_blank">IPython</a>
218 </span>
224 </span>
219 <span class="section_row_header">Links</span>
225 <span class="section_row_header">Links</span>
220 </div>
226 </div>
221 <div class="section_row">
227 <div class="section_row">
222 <span id="help_buttons1" class="section_row_buttons">
228 <span id="help_buttons1" class="section_row_buttons">
223 <a id="numpy_help" href="http://docs.scipy.org/doc/numpy/reference/" target="_blank">NumPy</a>
229 <a id="numpy_help" href="http://docs.scipy.org/doc/numpy/reference/" target="_blank">NumPy</a>
224 <a id="scipy_help" href="http://docs.scipy.org/doc/scipy/reference/" target="_blank">SciPy</a>
230 <a id="scipy_help" href="http://docs.scipy.org/doc/scipy/reference/" target="_blank">SciPy</a>
225 </span>
231 </span>
226 </div>
232 </div>
227 <div class="section_row">
233 <div class="section_row">
228 <span id="help_buttons2" class="section_row_buttons">
234 <span id="help_buttons2" class="section_row_buttons">
229 <a id="matplotlib_help" href="http://matplotlib.sourceforge.net/" target="_blank">MPL</a>
235 <a id="matplotlib_help" href="http://matplotlib.sourceforge.net/" target="_blank">MPL</a>
230 <a id="sympy_help" href="http://docs.sympy.org/dev/index.html" target="_blank">SymPy</a>
236 <a id="sympy_help" href="http://docs.sympy.org/dev/index.html" target="_blank">SymPy</a>
231 </span>
237 </span>
232 </div>
238 </div>
233 <div class="section_row">
239 <div class="section_row">
234 <span class="help_string">run selected cell</span>
240 <span class="help_string">run selected cell</span>
235 <span class="help_string_label">Shift-Enter :</span>
241 <span class="help_string_label">Shift-Enter :</span>
236 </div>
242 </div>
237 <div class="section_row">
243 <div class="section_row">
238 <span class="help_string">run selected cell in-place</span>
244 <span class="help_string">run selected cell in-place</span>
239 <span class="help_string_label">Ctrl-Enter :</span>
245 <span class="help_string_label">Ctrl-Enter :</span>
240 </div>
246 </div>
241 <div class="section_row">
247 <div class="section_row">
242 <span class="help_string">show keyboard shortcuts</span>
248 <span class="help_string">show keyboard shortcuts</span>
243 <span class="help_string_label">Ctrl-m h :</span>
249 <span class="help_string_label">Ctrl-m h :</span>
244 </div>
250 </div>
245 </div>
251 </div>
246 </div>
252 </div>
247
253
248 </div>
254 </div>
249 <div id="left_panel_splitter"></div>
255 <div id="left_panel_splitter"></div>
250 <div id="notebook_panel">
256 <div id="notebook_panel">
251 <div id="notebook"></div>
257 <div id="notebook"></div>
252 <div id="pager_splitter"></div>
258 <div id="pager_splitter"></div>
253 <div id="pager"></div>
259 <div id="pager"></div>
254 </div>
260 </div>
255
261
256 </div>
262 </div>
257
263
258 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
264 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
259 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
265 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
260 <script src="static/jquery/js/jquery.autogrow.js" type="text/javascript" charset="utf-8"></script>
266 <script src="static/jquery/js/jquery.autogrow.js" type="text/javascript" charset="utf-8"></script>
261
267
262 <script src="static/codemirror/lib/codemirror.js" charset="utf-8"></script>
268 <script src="static/codemirror/lib/codemirror.js" charset="utf-8"></script>
263 <script src="static/codemirror/mode/python/python.js" charset="utf-8"></script>
269 <script src="static/codemirror/mode/python/python.js" charset="utf-8"></script>
264 <script src="static/codemirror/mode/htmlmixed/htmlmixed.js" charset="utf-8"></script>
270 <script src="static/codemirror/mode/htmlmixed/htmlmixed.js" charset="utf-8"></script>
265 <script src="static/codemirror/mode/xml/xml.js" charset="utf-8"></script>
271 <script src="static/codemirror/mode/xml/xml.js" charset="utf-8"></script>
266 <script src="static/codemirror/mode/javascript/javascript.js" charset="utf-8"></script>
272 <script src="static/codemirror/mode/javascript/javascript.js" charset="utf-8"></script>
267 <script src="static/codemirror/mode/css/css.js" charset="utf-8"></script>
273 <script src="static/codemirror/mode/css/css.js" charset="utf-8"></script>
268 <script src="static/codemirror/mode/rst/rst.js" charset="utf-8"></script>
274 <script src="static/codemirror/mode/rst/rst.js" charset="utf-8"></script>
269 <script src="static/codemirror/mode/markdown/markdown.js" charset="utf-8"></script>
275 <script src="static/codemirror/mode/markdown/markdown.js" charset="utf-8"></script>
270
276
271 <script src="static/pagedown/Markdown.Converter.js" charset="utf-8"></script>
277 <script src="static/pagedown/Markdown.Converter.js" charset="utf-8"></script>
272
278
273 <script src="static/prettify/prettify.js" charset="utf-8"></script>
279 <script src="static/prettify/prettify.js" charset="utf-8"></script>
274
280
275 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
281 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
276 <script src="static/js/utils.js" type="text/javascript" charset="utf-8"></script>
282 <script src="static/js/utils.js" type="text/javascript" charset="utf-8"></script>
277 <script src="static/js/cell.js" type="text/javascript" charset="utf-8"></script>
283 <script src="static/js/cell.js" type="text/javascript" charset="utf-8"></script>
278 <script src="static/js/codecell.js" type="text/javascript" charset="utf-8"></script>
284 <script src="static/js/codecell.js" type="text/javascript" charset="utf-8"></script>
279 <script src="static/js/textcell.js" type="text/javascript" charset="utf-8"></script>
285 <script src="static/js/textcell.js" type="text/javascript" charset="utf-8"></script>
280 <script src="static/js/kernel.js" type="text/javascript" charset="utf-8"></script>
286 <script src="static/js/kernel.js" type="text/javascript" charset="utf-8"></script>
281 <script src="static/js/kernelstatus.js" type="text/javascript" charset="utf-8"></script>
287 <script src="static/js/kernelstatus.js" type="text/javascript" charset="utf-8"></script>
282 <script src="static/js/layout.js" type="text/javascript" charset="utf-8"></script>
288 <script src="static/js/layout.js" type="text/javascript" charset="utf-8"></script>
283 <script src="static/js/savewidget.js" type="text/javascript" charset="utf-8"></script>
289 <script src="static/js/savewidget.js" type="text/javascript" charset="utf-8"></script>
284 <script src="static/js/quickhelp.js" type="text/javascript" charset="utf-8"></script>
290 <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>
291 <script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
286 <script src="static/js/pager.js" type="text/javascript" charset="utf-8"></script>
292 <script src="static/js/pager.js" type="text/javascript" charset="utf-8"></script>
287 <script src="static/js/panelsection.js" type="text/javascript" charset="utf-8"></script>
293 <script src="static/js/panelsection.js" type="text/javascript" charset="utf-8"></script>
288 <script src="static/js/printwidget.js" type="text/javascript" charset="utf-8"></script>
294 <script src="static/js/printwidget.js" type="text/javascript" charset="utf-8"></script>
289 <script src="static/js/leftpanel.js" type="text/javascript" charset="utf-8"></script>
295 <script src="static/js/leftpanel.js" type="text/javascript" charset="utf-8"></script>
290 <script src="static/js/notebook.js" type="text/javascript" charset="utf-8"></script>
296 <script src="static/js/notebook.js" type="text/javascript" charset="utf-8"></script>
291 <script src="static/js/notebookmain.js" type="text/javascript" charset="utf-8"></script>
297 <script src="static/js/notebookmain.js" type="text/javascript" charset="utf-8"></script>
292
298
293 </body>
299 </body>
294
300
295 </html>
301 </html>
@@ -1,69 +1,36 b''
1 <!DOCTYPE HTML>
1 {% extends layout.html %}
2 <html>
3
2
4 <head>
3 {% block title %}
5 <meta charset="utf-8">
4 IPython Dashboard
5 {% end %}
6
6
7 <title>IPython Dashboard</title>
7 {% block stylesheet %}
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
13 <link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
8 <link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
9 {% end %}
14
10
11 {% block meta %}
15 <meta name="read_only" content="{{read_only}}"/>
12 <meta name="read_only" content="{{read_only}}"/>
16
13 {% end %}
17 </head>
14
18
15 {% block params %}
19 <body data-project={{project}} data-base-project-url={{base_project_url}}
16 data-project={{project}}
20 data-base-kernel-url={{base_kernel_url}}>
17 data-base-project-url={{base_project_url}}
21
18 data-base-kernel-url={{base_kernel_url}}
22 <div id="header">
19 {% end %}
23 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
20
24 <span id="login_widget" class="hidden">
21 {% block content_panel %}
25 <button id="login">Login</button>
22 <div id="content_toolbar">
26 </span>
23 <span id="drag_info">Drag files onto the list to import notebooks.</span>
27 </div>
24 <span id="notebooks_buttons">
28
25 <button id="new_notebook">New Notebook</button>
29 <div id="header_border"></div>
26 </span>
30
31 <div id="main_app">
32
33 <div id="app_hbox">
34
35 <div id="left_panel">
36 </div>
37
38 <div id="content_panel">
39 <div id="content_toolbar">
40 <span id="drag_info">Drag files onto the list to import notebooks.</span>
41 <span id="notebooks_buttons">
42 <button id="new_notebook">New Notebook</button>
43 </span>
44 </div>
45 <div id="notebook_list">
46 <div id="project_name"><h2>{{project}}</h2></div>
47 </div>
48
49 </div>
27 </div>
50
28 <div id="notebook_list">
51 <div id="right_panel">
29 <div id="project_name"><h2>{{project}}</h2></div>
52 </div>
30 </div>
53
31 {% end %}
54 </div>
55
56 </div>
57
58 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
59 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
60 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
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>
63 <script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
64
65 </body>
66
67 </html>
68
69
32
33 {% block script %}
34 <script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
35 <script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
36 {% end %}
General Comments 0
You need to be logged in to leave comments. Login now