##// END OF EJS Templates
rename kernel_status -> kernel_id
Matthias BUSSONNIER -
Show More
@@ -1,739 +1,739 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 time
21 import time
22 import uuid
22 import uuid
23
23
24 from tornado import web
24 from tornado import web
25 from tornado import websocket
25 from tornado import websocket
26
26
27 from zmq.eventloop import ioloop
27 from zmq.eventloop import ioloop
28 from zmq.utils import jsonapi
28 from zmq.utils import jsonapi
29
29
30 from IPython.external.decorator import decorator
30 from IPython.external.decorator import decorator
31 from IPython.zmq.session import Session
31 from IPython.zmq.session import Session
32 from IPython.lib.security import passwd_check
32 from IPython.lib.security import passwd_check
33
33
34 try:
34 try:
35 from docutils.core import publish_string
35 from docutils.core import publish_string
36 except ImportError:
36 except ImportError:
37 publish_string = None
37 publish_string = None
38
38
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42
42
43 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # Google Chrome, as of release 16, changed its websocket protocol number. The
44 # parts tornado cares about haven't really changed, so it's OK to continue
44 # parts tornado cares about haven't really changed, so it's OK to continue
45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
46 # version as of Oct 30/2011) the version check fails, see the issue report:
46 # version as of Oct 30/2011) the version check fails, see the issue report:
47
47
48 # https://github.com/facebook/tornado/issues/385
48 # https://github.com/facebook/tornado/issues/385
49
49
50 # This issue has been fixed in Tornado post 2.1.1:
50 # This issue has been fixed in Tornado post 2.1.1:
51
51
52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
53
53
54 # Here we manually apply the same patch as above so that users of IPython can
54 # Here we manually apply the same patch as above so that users of IPython can
55 # continue to work with an officially released Tornado. We make the
55 # continue to work with an officially released Tornado. We make the
56 # monkeypatch version check as narrow as possible to limit its effects; once
56 # monkeypatch version check as narrow as possible to limit its effects; once
57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
58
58
59 import tornado
59 import tornado
60
60
61 if tornado.version_info <= (2,1,1):
61 if tornado.version_info <= (2,1,1):
62
62
63 def _execute(self, transforms, *args, **kwargs):
63 def _execute(self, transforms, *args, **kwargs):
64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
65
65
66 self.open_args = args
66 self.open_args = args
67 self.open_kwargs = kwargs
67 self.open_kwargs = kwargs
68
68
69 # The difference between version 8 and 13 is that in 8 the
69 # The difference between version 8 and 13 is that in 8 the
70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
71 # simply "Origin".
71 # simply "Origin".
72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
73 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection = WebSocketProtocol8(self)
74 self.ws_connection.accept_connection()
74 self.ws_connection.accept_connection()
75
75
76 elif self.request.headers.get("Sec-WebSocket-Version"):
76 elif self.request.headers.get("Sec-WebSocket-Version"):
77 self.stream.write(tornado.escape.utf8(
77 self.stream.write(tornado.escape.utf8(
78 "HTTP/1.1 426 Upgrade Required\r\n"
78 "HTTP/1.1 426 Upgrade Required\r\n"
79 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 "Sec-WebSocket-Version: 8\r\n\r\n"))
80 self.stream.close()
80 self.stream.close()
81
81
82 else:
82 else:
83 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection = WebSocketProtocol76(self)
84 self.ws_connection.accept_connection()
84 self.ws_connection.accept_connection()
85
85
86 websocket.WebSocketHandler._execute = _execute
86 websocket.WebSocketHandler._execute = _execute
87 del _execute
87 del _execute
88
88
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90 # Decorator for disabling read-only handlers
90 # Decorator for disabling read-only handlers
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92
92
93 @decorator
93 @decorator
94 def not_if_readonly(f, self, *args, **kwargs):
94 def not_if_readonly(f, self, *args, **kwargs):
95 if self.application.read_only:
95 if self.application.read_only:
96 raise web.HTTPError(403, "Notebook server is read-only")
96 raise web.HTTPError(403, "Notebook server is read-only")
97 else:
97 else:
98 return f(self, *args, **kwargs)
98 return f(self, *args, **kwargs)
99
99
100 @decorator
100 @decorator
101 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 def authenticate_unless_readonly(f, self, *args, **kwargs):
102 """authenticate this page *unless* readonly view is active.
102 """authenticate this page *unless* readonly view is active.
103
103
104 In read-only mode, the notebook list and print view should
104 In read-only mode, the notebook list and print view should
105 be accessible without authentication.
105 be accessible without authentication.
106 """
106 """
107
107
108 @web.authenticated
108 @web.authenticated
109 def auth_f(self, *args, **kwargs):
109 def auth_f(self, *args, **kwargs):
110 return f(self, *args, **kwargs)
110 return f(self, *args, **kwargs)
111
111
112 if self.application.read_only:
112 if self.application.read_only:
113 return f(self, *args, **kwargs)
113 return f(self, *args, **kwargs)
114 else:
114 else:
115 return auth_f(self, *args, **kwargs)
115 return auth_f(self, *args, **kwargs)
116
116
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118 # Top-level handlers
118 # Top-level handlers
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120
120
121 class RequestHandler(web.RequestHandler):
121 class RequestHandler(web.RequestHandler):
122 """RequestHandler with default variable setting."""
122 """RequestHandler with default variable setting."""
123
123
124 def render(*args, **kwargs):
124 def render(*args, **kwargs):
125 kwargs.setdefault('message', '')
125 kwargs.setdefault('message', '')
126 return web.RequestHandler.render(*args, **kwargs)
126 return web.RequestHandler.render(*args, **kwargs)
127
127
128 class AuthenticatedHandler(RequestHandler):
128 class AuthenticatedHandler(RequestHandler):
129 """A RequestHandler with an authenticated user."""
129 """A RequestHandler with an authenticated user."""
130
130
131 def get_current_user(self):
131 def get_current_user(self):
132 user_id = self.get_secure_cookie("username")
132 user_id = self.get_secure_cookie("username")
133 # For now the user_id should not return empty, but it could eventually
133 # For now the user_id should not return empty, but it could eventually
134 if user_id == '':
134 if user_id == '':
135 user_id = 'anonymous'
135 user_id = 'anonymous'
136 if user_id is None:
136 if user_id is None:
137 # prevent extra Invalid cookie sig warnings:
137 # prevent extra Invalid cookie sig warnings:
138 self.clear_cookie('username')
138 self.clear_cookie('username')
139 if not self.application.password and not self.application.read_only:
139 if not self.application.password and not self.application.read_only:
140 user_id = 'anonymous'
140 user_id = 'anonymous'
141 return user_id
141 return user_id
142
142
143 @property
143 @property
144 def logged_in(self):
144 def logged_in(self):
145 """Is a user currently logged in?
145 """Is a user currently logged in?
146
146
147 """
147 """
148 user = self.get_current_user()
148 user = self.get_current_user()
149 return (user and not user == 'anonymous')
149 return (user and not user == 'anonymous')
150
150
151 @property
151 @property
152 def login_available(self):
152 def login_available(self):
153 """May a user proceed to log in?
153 """May a user proceed to log in?
154
154
155 This returns True if login capability is available, irrespective of
155 This returns True if login capability is available, irrespective of
156 whether the user is already logged in or not.
156 whether the user is already logged in or not.
157
157
158 """
158 """
159 return bool(self.application.password)
159 return bool(self.application.password)
160
160
161 @property
161 @property
162 def read_only(self):
162 def read_only(self):
163 """Is the notebook read-only?
163 """Is the notebook read-only?
164
164
165 """
165 """
166 return self.application.read_only
166 return self.application.read_only
167
167
168 @property
168 @property
169 def ws_url(self):
169 def ws_url(self):
170 """websocket url matching the current request
170 """websocket url matching the current request
171
171
172 turns http[s]://host[:port] into
172 turns http[s]://host[:port] into
173 ws[s]://host[:port]
173 ws[s]://host[:port]
174 """
174 """
175 proto = self.request.protocol.replace('http', 'ws')
175 proto = self.request.protocol.replace('http', 'ws')
176 host = self.application.ipython_app.websocket_host # default to config value
176 host = self.application.ipython_app.websocket_host # default to config value
177 if host == '':
177 if host == '':
178 host = self.request.host # get from request
178 host = self.request.host # get from request
179 return "%s://%s" % (proto, host)
179 return "%s://%s" % (proto, host)
180
180
181
181
182 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
182 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
183 """static files should only be accessible when logged in"""
183 """static files should only be accessible when logged in"""
184
184
185 @authenticate_unless_readonly
185 @authenticate_unless_readonly
186 def get(self, path):
186 def get(self, path):
187 return web.StaticFileHandler.get(self, path)
187 return web.StaticFileHandler.get(self, path)
188
188
189
189
190 class ProjectDashboardHandler(AuthenticatedHandler):
190 class ProjectDashboardHandler(AuthenticatedHandler):
191
191
192 @authenticate_unless_readonly
192 @authenticate_unless_readonly
193 def get(self):
193 def get(self):
194 nbm = self.application.notebook_manager
194 nbm = self.application.notebook_manager
195 project = nbm.notebook_dir
195 project = nbm.notebook_dir
196 self.render(
196 self.render(
197 'projectdashboard.html', project=project,
197 'projectdashboard.html', project=project,
198 base_project_url=self.application.ipython_app.base_project_url,
198 base_project_url=self.application.ipython_app.base_project_url,
199 base_kernel_url=self.application.ipython_app.base_kernel_url,
199 base_kernel_url=self.application.ipython_app.base_kernel_url,
200 read_only=self.read_only,
200 read_only=self.read_only,
201 logged_in=self.logged_in,
201 logged_in=self.logged_in,
202 login_available=self.login_available
202 login_available=self.login_available
203 )
203 )
204
204
205
205
206 class LoginHandler(AuthenticatedHandler):
206 class LoginHandler(AuthenticatedHandler):
207
207
208 def _render(self, message=None):
208 def _render(self, message=None):
209 self.render('login.html',
209 self.render('login.html',
210 next=self.get_argument('next', default='/'),
210 next=self.get_argument('next', default='/'),
211 read_only=self.read_only,
211 read_only=self.read_only,
212 logged_in=self.logged_in,
212 logged_in=self.logged_in,
213 login_available=self.login_available,
213 login_available=self.login_available,
214 base_project_url=self.application.ipython_app.base_project_url,
214 base_project_url=self.application.ipython_app.base_project_url,
215 message=message
215 message=message
216 )
216 )
217
217
218 def get(self):
218 def get(self):
219 if self.current_user:
219 if self.current_user:
220 self.redirect(self.get_argument('next', default='/'))
220 self.redirect(self.get_argument('next', default='/'))
221 else:
221 else:
222 self._render()
222 self._render()
223
223
224 def post(self):
224 def post(self):
225 pwd = self.get_argument('password', default=u'')
225 pwd = self.get_argument('password', default=u'')
226 if self.application.password:
226 if self.application.password:
227 if passwd_check(self.application.password, pwd):
227 if passwd_check(self.application.password, pwd):
228 self.set_secure_cookie('username', str(uuid.uuid4()))
228 self.set_secure_cookie('username', str(uuid.uuid4()))
229 else:
229 else:
230 self._render(message={'error': 'Invalid password'})
230 self._render(message={'error': 'Invalid password'})
231 return
231 return
232
232
233 self.redirect(self.get_argument('next', default='/'))
233 self.redirect(self.get_argument('next', default='/'))
234
234
235
235
236 class LogoutHandler(AuthenticatedHandler):
236 class LogoutHandler(AuthenticatedHandler):
237
237
238 def get(self):
238 def get(self):
239 self.clear_cookie('username')
239 self.clear_cookie('username')
240 if self.login_available:
240 if self.login_available:
241 message = {'info': 'Successfully logged out.'}
241 message = {'info': 'Successfully logged out.'}
242 else:
242 else:
243 message = {'warning': 'Cannot log out. Notebook authentication '
243 message = {'warning': 'Cannot log out. Notebook authentication '
244 'is disabled.'}
244 'is disabled.'}
245
245
246 self.render('logout.html',
246 self.render('logout.html',
247 read_only=self.read_only,
247 read_only=self.read_only,
248 logged_in=self.logged_in,
248 logged_in=self.logged_in,
249 login_available=self.login_available,
249 login_available=self.login_available,
250 base_project_url=self.application.ipython_app.base_project_url,
250 base_project_url=self.application.ipython_app.base_project_url,
251 message=message)
251 message=message)
252
252
253
253
254 class NewHandler(AuthenticatedHandler):
254 class NewHandler(AuthenticatedHandler):
255
255
256 @web.authenticated
256 @web.authenticated
257 def get(self):
257 def get(self):
258 nbm = self.application.notebook_manager
258 nbm = self.application.notebook_manager
259 project = nbm.notebook_dir
259 project = nbm.notebook_dir
260 notebook_id = nbm.new_notebook()
260 notebook_id = nbm.new_notebook()
261 self.render(
261 self.render(
262 'notebook.html', project=project,
262 'notebook.html', project=project,
263 notebook_id=notebook_id,
263 notebook_id=notebook_id,
264 base_project_url=self.application.ipython_app.base_project_url,
264 base_project_url=self.application.ipython_app.base_project_url,
265 base_kernel_url=self.application.ipython_app.base_kernel_url,
265 base_kernel_url=self.application.ipython_app.base_kernel_url,
266 kill_kernel=False,
266 kill_kernel=False,
267 read_only=False,
267 read_only=False,
268 logged_in=self.logged_in,
268 logged_in=self.logged_in,
269 login_available=self.login_available,
269 login_available=self.login_available,
270 mathjax_url=self.application.ipython_app.mathjax_url,
270 mathjax_url=self.application.ipython_app.mathjax_url,
271 )
271 )
272
272
273
273
274 class NamedNotebookHandler(AuthenticatedHandler):
274 class NamedNotebookHandler(AuthenticatedHandler):
275
275
276 @authenticate_unless_readonly
276 @authenticate_unless_readonly
277 def get(self, notebook_id):
277 def get(self, notebook_id):
278 nbm = self.application.notebook_manager
278 nbm = self.application.notebook_manager
279 project = nbm.notebook_dir
279 project = nbm.notebook_dir
280 if not nbm.notebook_exists(notebook_id):
280 if not nbm.notebook_exists(notebook_id):
281 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
281 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
282
282
283 self.render(
283 self.render(
284 'notebook.html', project=project,
284 'notebook.html', project=project,
285 notebook_id=notebook_id,
285 notebook_id=notebook_id,
286 base_project_url=self.application.ipython_app.base_project_url,
286 base_project_url=self.application.ipython_app.base_project_url,
287 base_kernel_url=self.application.ipython_app.base_kernel_url,
287 base_kernel_url=self.application.ipython_app.base_kernel_url,
288 kill_kernel=False,
288 kill_kernel=False,
289 read_only=self.read_only,
289 read_only=self.read_only,
290 logged_in=self.logged_in,
290 logged_in=self.logged_in,
291 login_available=self.login_available,
291 login_available=self.login_available,
292 mathjax_url=self.application.ipython_app.mathjax_url,
292 mathjax_url=self.application.ipython_app.mathjax_url,
293 )
293 )
294
294
295
295
296 class PrintNotebookHandler(AuthenticatedHandler):
296 class PrintNotebookHandler(AuthenticatedHandler):
297
297
298 @authenticate_unless_readonly
298 @authenticate_unless_readonly
299 def get(self, notebook_id):
299 def get(self, notebook_id):
300 nbm = self.application.notebook_manager
300 nbm = self.application.notebook_manager
301 project = nbm.notebook_dir
301 project = nbm.notebook_dir
302 if not nbm.notebook_exists(notebook_id):
302 if not nbm.notebook_exists(notebook_id):
303 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
303 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
304
304
305 self.render(
305 self.render(
306 'printnotebook.html', project=project,
306 'printnotebook.html', project=project,
307 notebook_id=notebook_id,
307 notebook_id=notebook_id,
308 base_project_url=self.application.ipython_app.base_project_url,
308 base_project_url=self.application.ipython_app.base_project_url,
309 base_kernel_url=self.application.ipython_app.base_kernel_url,
309 base_kernel_url=self.application.ipython_app.base_kernel_url,
310 kill_kernel=False,
310 kill_kernel=False,
311 read_only=self.read_only,
311 read_only=self.read_only,
312 logged_in=self.logged_in,
312 logged_in=self.logged_in,
313 login_available=self.login_available,
313 login_available=self.login_available,
314 mathjax_url=self.application.ipython_app.mathjax_url,
314 mathjax_url=self.application.ipython_app.mathjax_url,
315 )
315 )
316
316
317 #-----------------------------------------------------------------------------
317 #-----------------------------------------------------------------------------
318 # Kernel handlers
318 # Kernel handlers
319 #-----------------------------------------------------------------------------
319 #-----------------------------------------------------------------------------
320
320
321
321
322 class MainKernelHandler(AuthenticatedHandler):
322 class MainKernelHandler(AuthenticatedHandler):
323
323
324 @web.authenticated
324 @web.authenticated
325 def get(self):
325 def get(self):
326 km = self.application.kernel_manager
326 km = self.application.kernel_manager
327 self.finish(jsonapi.dumps(km.kernel_ids))
327 self.finish(jsonapi.dumps(km.kernel_ids))
328
328
329 @web.authenticated
329 @web.authenticated
330 def post(self):
330 def post(self):
331 km = self.application.kernel_manager
331 km = self.application.kernel_manager
332 notebook_id = self.get_argument('notebook', default=None)
332 notebook_id = self.get_argument('notebook', default=None)
333 kernel_id = km.start_kernel(notebook_id)
333 kernel_id = km.start_kernel(notebook_id)
334 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
334 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
335 self.set_header('Location', '/'+kernel_id)
335 self.set_header('Location', '/'+kernel_id)
336 self.finish(jsonapi.dumps(data))
336 self.finish(jsonapi.dumps(data))
337
337
338
338
339 class KernelHandler(AuthenticatedHandler):
339 class KernelHandler(AuthenticatedHandler):
340
340
341 SUPPORTED_METHODS = ('DELETE')
341 SUPPORTED_METHODS = ('DELETE')
342
342
343 @web.authenticated
343 @web.authenticated
344 def delete(self, kernel_id):
344 def delete(self, kernel_id):
345 km = self.application.kernel_manager
345 km = self.application.kernel_manager
346 km.kill_kernel(kernel_id)
346 km.kill_kernel(kernel_id)
347 self.set_status(204)
347 self.set_status(204)
348 self.finish()
348 self.finish()
349
349
350
350
351 class KernelActionHandler(AuthenticatedHandler):
351 class KernelActionHandler(AuthenticatedHandler):
352
352
353 @web.authenticated
353 @web.authenticated
354 def post(self, kernel_id, action):
354 def post(self, kernel_id, action):
355 km = self.application.kernel_manager
355 km = self.application.kernel_manager
356 if action == 'interrupt':
356 if action == 'interrupt':
357 km.interrupt_kernel(kernel_id)
357 km.interrupt_kernel(kernel_id)
358 self.set_status(204)
358 self.set_status(204)
359 if action == 'restart':
359 if action == 'restart':
360 new_kernel_id = km.restart_kernel(kernel_id)
360 new_kernel_id = km.restart_kernel(kernel_id)
361 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
361 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
362 self.set_header('Location', '/'+new_kernel_id)
362 self.set_header('Location', '/'+new_kernel_id)
363 self.write(jsonapi.dumps(data))
363 self.write(jsonapi.dumps(data))
364 self.finish()
364 self.finish()
365
365
366
366
367 class ZMQStreamHandler(websocket.WebSocketHandler):
367 class ZMQStreamHandler(websocket.WebSocketHandler):
368
368
369 def _reserialize_reply(self, msg_list):
369 def _reserialize_reply(self, msg_list):
370 """Reserialize a reply message using JSON.
370 """Reserialize a reply message using JSON.
371
371
372 This takes the msg list from the ZMQ socket, unserializes it using
372 This takes the msg list from the ZMQ socket, unserializes it using
373 self.session and then serializes the result using JSON. This method
373 self.session and then serializes the result using JSON. This method
374 should be used by self._on_zmq_reply to build messages that can
374 should be used by self._on_zmq_reply to build messages that can
375 be sent back to the browser.
375 be sent back to the browser.
376 """
376 """
377 idents, msg_list = self.session.feed_identities(msg_list)
377 idents, msg_list = self.session.feed_identities(msg_list)
378 msg = self.session.unserialize(msg_list)
378 msg = self.session.unserialize(msg_list)
379 try:
379 try:
380 msg['header'].pop('date')
380 msg['header'].pop('date')
381 except KeyError:
381 except KeyError:
382 pass
382 pass
383 try:
383 try:
384 msg['parent_header'].pop('date')
384 msg['parent_header'].pop('date')
385 except KeyError:
385 except KeyError:
386 pass
386 pass
387 msg.pop('buffers')
387 msg.pop('buffers')
388 return jsonapi.dumps(msg)
388 return jsonapi.dumps(msg)
389
389
390 def _on_zmq_reply(self, msg_list):
390 def _on_zmq_reply(self, msg_list):
391 try:
391 try:
392 msg = self._reserialize_reply(msg_list)
392 msg = self._reserialize_reply(msg_list)
393 except:
393 except:
394 self.application.log.critical("Malformed message: %r" % msg_list)
394 self.application.log.critical("Malformed message: %r" % msg_list)
395 else:
395 else:
396 self.write_message(msg)
396 self.write_message(msg)
397
397
398 def allow_draft76(self):
398 def allow_draft76(self):
399 """Allow draft 76, until browsers such as Safari update to RFC 6455.
399 """Allow draft 76, until browsers such as Safari update to RFC 6455.
400
400
401 This has been disabled by default in tornado in release 2.2.0, and
401 This has been disabled by default in tornado in release 2.2.0, and
402 support will be removed in later versions.
402 support will be removed in later versions.
403 """
403 """
404 return True
404 return True
405
405
406
406
407 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
407 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
408
408
409 def open(self, kernel_id):
409 def open(self, kernel_id):
410 self.kernel_id = kernel_id.decode('ascii')
410 self.kernel_id = kernel_id.decode('ascii')
411 try:
411 try:
412 cfg = self.application.ipython_app.config
412 cfg = self.application.ipython_app.config
413 except AttributeError:
413 except AttributeError:
414 # protect from the case where this is run from something other than
414 # protect from the case where this is run from something other than
415 # the notebook app:
415 # the notebook app:
416 cfg = None
416 cfg = None
417 self.session = Session(config=cfg)
417 self.session = Session(config=cfg)
418 self.save_on_message = self.on_message
418 self.save_on_message = self.on_message
419 self.on_message = self.on_first_message
419 self.on_message = self.on_first_message
420
420
421 def get_current_user(self):
421 def get_current_user(self):
422 user_id = self.get_secure_cookie("username")
422 user_id = self.get_secure_cookie("username")
423 if user_id == '' or (user_id is None and not self.application.password):
423 if user_id == '' or (user_id is None and not self.application.password):
424 user_id = 'anonymous'
424 user_id = 'anonymous'
425 return user_id
425 return user_id
426
426
427 def _inject_cookie_message(self, msg):
427 def _inject_cookie_message(self, msg):
428 """Inject the first message, which is the document cookie,
428 """Inject the first message, which is the document cookie,
429 for authentication."""
429 for authentication."""
430 if isinstance(msg, unicode):
430 if isinstance(msg, unicode):
431 # Cookie can't constructor doesn't accept unicode strings for some reason
431 # Cookie can't constructor doesn't accept unicode strings for some reason
432 msg = msg.encode('utf8', 'replace')
432 msg = msg.encode('utf8', 'replace')
433 try:
433 try:
434 self.request._cookies = Cookie.SimpleCookie(msg)
434 self.request._cookies = Cookie.SimpleCookie(msg)
435 except:
435 except:
436 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
436 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
437
437
438 def on_first_message(self, msg):
438 def on_first_message(self, msg):
439 self._inject_cookie_message(msg)
439 self._inject_cookie_message(msg)
440 if self.get_current_user() is None:
440 if self.get_current_user() is None:
441 logging.warn("Couldn't authenticate WebSocket connection")
441 logging.warn("Couldn't authenticate WebSocket connection")
442 raise web.HTTPError(403)
442 raise web.HTTPError(403)
443 self.on_message = self.save_on_message
443 self.on_message = self.save_on_message
444
444
445
445
446 class IOPubHandler(AuthenticatedZMQStreamHandler):
446 class IOPubHandler(AuthenticatedZMQStreamHandler):
447
447
448 def initialize(self, *args, **kwargs):
448 def initialize(self, *args, **kwargs):
449 self._kernel_alive = True
449 self._kernel_alive = True
450 self._beating = False
450 self._beating = False
451 self.iopub_stream = None
451 self.iopub_stream = None
452 self.hb_stream = None
452 self.hb_stream = None
453
453
454 def on_first_message(self, msg):
454 def on_first_message(self, msg):
455 try:
455 try:
456 super(IOPubHandler, self).on_first_message(msg)
456 super(IOPubHandler, self).on_first_message(msg)
457 except web.HTTPError:
457 except web.HTTPError:
458 self.close()
458 self.close()
459 return
459 return
460 km = self.application.kernel_manager
460 km = self.application.kernel_manager
461 self.time_to_dead = km.time_to_dead
461 self.time_to_dead = km.time_to_dead
462 self.first_beat = km.first_beat
462 self.first_beat = km.first_beat
463 kernel_id = self.kernel_id
463 kernel_id = self.kernel_id
464 try:
464 try:
465 self.iopub_stream = km.create_iopub_stream(kernel_id)
465 self.iopub_stream = km.create_iopub_stream(kernel_id)
466 self.hb_stream = km.create_hb_stream(kernel_id)
466 self.hb_stream = km.create_hb_stream(kernel_id)
467 except web.HTTPError:
467 except web.HTTPError:
468 # WebSockets don't response to traditional error codes so we
468 # WebSockets don't response to traditional error codes so we
469 # close the connection.
469 # close the connection.
470 if not self.stream.closed():
470 if not self.stream.closed():
471 self.stream.close()
471 self.stream.close()
472 self.close()
472 self.close()
473 else:
473 else:
474 self.iopub_stream.on_recv(self._on_zmq_reply)
474 self.iopub_stream.on_recv(self._on_zmq_reply)
475 self.start_hb(self.kernel_died)
475 self.start_hb(self.kernel_died)
476
476
477 def on_message(self, msg):
477 def on_message(self, msg):
478 pass
478 pass
479
479
480 def on_close(self):
480 def on_close(self):
481 # This method can be called twice, once by self.kernel_died and once
481 # This method can be called twice, once by self.kernel_died and once
482 # from the WebSocket close event. If the WebSocket connection is
482 # from the WebSocket close event. If the WebSocket connection is
483 # closed before the ZMQ streams are setup, they could be None.
483 # closed before the ZMQ streams are setup, they could be None.
484 self.stop_hb()
484 self.stop_hb()
485 if self.iopub_stream is not None and not self.iopub_stream.closed():
485 if self.iopub_stream is not None and not self.iopub_stream.closed():
486 self.iopub_stream.on_recv(None)
486 self.iopub_stream.on_recv(None)
487 self.iopub_stream.close()
487 self.iopub_stream.close()
488 if self.hb_stream is not None and not self.hb_stream.closed():
488 if self.hb_stream is not None and not self.hb_stream.closed():
489 self.hb_stream.close()
489 self.hb_stream.close()
490
490
491 def start_hb(self, callback):
491 def start_hb(self, callback):
492 """Start the heartbeating and call the callback if the kernel dies."""
492 """Start the heartbeating and call the callback if the kernel dies."""
493 if not self._beating:
493 if not self._beating:
494 self._kernel_alive = True
494 self._kernel_alive = True
495
495
496 def ping_or_dead():
496 def ping_or_dead():
497 self.hb_stream.flush()
497 self.hb_stream.flush()
498 if self._kernel_alive:
498 if self._kernel_alive:
499 self._kernel_alive = False
499 self._kernel_alive = False
500 self.hb_stream.send(b'ping')
500 self.hb_stream.send(b'ping')
501 # flush stream to force immediate socket send
501 # flush stream to force immediate socket send
502 self.hb_stream.flush()
502 self.hb_stream.flush()
503 else:
503 else:
504 try:
504 try:
505 callback()
505 callback()
506 except:
506 except:
507 pass
507 pass
508 finally:
508 finally:
509 self.stop_hb()
509 self.stop_hb()
510
510
511 def beat_received(msg):
511 def beat_received(msg):
512 self._kernel_alive = True
512 self._kernel_alive = True
513
513
514 self.hb_stream.on_recv(beat_received)
514 self.hb_stream.on_recv(beat_received)
515 loop = ioloop.IOLoop.instance()
515 loop = ioloop.IOLoop.instance()
516 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
516 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
517 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
517 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
518 self._beating= True
518 self._beating= True
519
519
520 def _really_start_hb(self):
520 def _really_start_hb(self):
521 """callback for delayed heartbeat start
521 """callback for delayed heartbeat start
522
522
523 Only start the hb loop if we haven't been closed during the wait.
523 Only start the hb loop if we haven't been closed during the wait.
524 """
524 """
525 if self._beating and not self.hb_stream.closed():
525 if self._beating and not self.hb_stream.closed():
526 self._hb_periodic_callback.start()
526 self._hb_periodic_callback.start()
527
527
528 def stop_hb(self):
528 def stop_hb(self):
529 """Stop the heartbeating and cancel all related callbacks."""
529 """Stop the heartbeating and cancel all related callbacks."""
530 if self._beating:
530 if self._beating:
531 self._beating = False
531 self._beating = False
532 self._hb_periodic_callback.stop()
532 self._hb_periodic_callback.stop()
533 if not self.hb_stream.closed():
533 if not self.hb_stream.closed():
534 self.hb_stream.on_recv(None)
534 self.hb_stream.on_recv(None)
535
535
536 def kernel_died(self):
536 def kernel_died(self):
537 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
537 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
538 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
538 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
539 self.write_message(
539 self.write_message(
540 {'header': {'msg_type': 'status'},
540 {'header': {'msg_type': 'status'},
541 'parent_header': {},
541 'parent_header': {},
542 'content': {'execution_state':'dead'}
542 'content': {'execution_state':'dead'}
543 }
543 }
544 )
544 )
545 self.on_close()
545 self.on_close()
546
546
547
547
548 class ShellHandler(AuthenticatedZMQStreamHandler):
548 class ShellHandler(AuthenticatedZMQStreamHandler):
549
549
550 def initialize(self, *args, **kwargs):
550 def initialize(self, *args, **kwargs):
551 self.shell_stream = None
551 self.shell_stream = None
552
552
553 def on_first_message(self, msg):
553 def on_first_message(self, msg):
554 try:
554 try:
555 super(ShellHandler, self).on_first_message(msg)
555 super(ShellHandler, self).on_first_message(msg)
556 except web.HTTPError:
556 except web.HTTPError:
557 self.close()
557 self.close()
558 return
558 return
559 km = self.application.kernel_manager
559 km = self.application.kernel_manager
560 self.max_msg_size = km.max_msg_size
560 self.max_msg_size = km.max_msg_size
561 kernel_id = self.kernel_id
561 kernel_id = self.kernel_id
562 try:
562 try:
563 self.shell_stream = km.create_shell_stream(kernel_id)
563 self.shell_stream = km.create_shell_stream(kernel_id)
564 except web.HTTPError:
564 except web.HTTPError:
565 # WebSockets don't response to traditional error codes so we
565 # WebSockets don't response to traditional error codes so we
566 # close the connection.
566 # close the connection.
567 if not self.stream.closed():
567 if not self.stream.closed():
568 self.stream.close()
568 self.stream.close()
569 self.close()
569 self.close()
570 else:
570 else:
571 self.shell_stream.on_recv(self._on_zmq_reply)
571 self.shell_stream.on_recv(self._on_zmq_reply)
572
572
573 def on_message(self, msg):
573 def on_message(self, msg):
574 if len(msg) < self.max_msg_size:
574 if len(msg) < self.max_msg_size:
575 msg = jsonapi.loads(msg)
575 msg = jsonapi.loads(msg)
576 self.session.send(self.shell_stream, msg)
576 self.session.send(self.shell_stream, msg)
577
577
578 def on_close(self):
578 def on_close(self):
579 # Make sure the stream exists and is not already closed.
579 # Make sure the stream exists and is not already closed.
580 if self.shell_stream is not None and not self.shell_stream.closed():
580 if self.shell_stream is not None and not self.shell_stream.closed():
581 self.shell_stream.close()
581 self.shell_stream.close()
582
582
583
583
584 #-----------------------------------------------------------------------------
584 #-----------------------------------------------------------------------------
585 # Notebook web service handlers
585 # Notebook web service handlers
586 #-----------------------------------------------------------------------------
586 #-----------------------------------------------------------------------------
587
587
588 class NotebookRootHandler(AuthenticatedHandler):
588 class NotebookRootHandler(AuthenticatedHandler):
589
589
590 @authenticate_unless_readonly
590 @authenticate_unless_readonly
591 def get(self):
591 def get(self):
592 nbm = self.application.notebook_manager
592 nbm = self.application.notebook_manager
593 km = self.application.kernel_manager
593 km = self.application.kernel_manager
594 files = nbm.list_notebooks()
594 files = nbm.list_notebooks()
595 for f in files :
595 for f in files :
596 nid = f['notebook_id']
596 nid = f['notebook_id']
597 kid = km.kernel_for_notebook(nid)
597 kid = km.kernel_for_notebook(nid)
598 if kid is not None:
598 if kid is not None:
599 f['kernel_status']=kid
599 f['kernel_id']=kid
600 self.finish(jsonapi.dumps(files))
600 self.finish(jsonapi.dumps(files))
601
601
602 @web.authenticated
602 @web.authenticated
603 def post(self):
603 def post(self):
604 nbm = self.application.notebook_manager
604 nbm = self.application.notebook_manager
605 body = self.request.body.strip()
605 body = self.request.body.strip()
606 format = self.get_argument('format', default='json')
606 format = self.get_argument('format', default='json')
607 name = self.get_argument('name', default=None)
607 name = self.get_argument('name', default=None)
608 if body:
608 if body:
609 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
609 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
610 else:
610 else:
611 notebook_id = nbm.new_notebook()
611 notebook_id = nbm.new_notebook()
612 self.set_header('Location', '/'+notebook_id)
612 self.set_header('Location', '/'+notebook_id)
613 self.finish(jsonapi.dumps(notebook_id))
613 self.finish(jsonapi.dumps(notebook_id))
614
614
615
615
616 class NotebookHandler(AuthenticatedHandler):
616 class NotebookHandler(AuthenticatedHandler):
617
617
618 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
618 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
619
619
620 @authenticate_unless_readonly
620 @authenticate_unless_readonly
621 def get(self, notebook_id):
621 def get(self, notebook_id):
622 nbm = self.application.notebook_manager
622 nbm = self.application.notebook_manager
623 format = self.get_argument('format', default='json')
623 format = self.get_argument('format', default='json')
624 last_mod, name, data = nbm.get_notebook(notebook_id, format)
624 last_mod, name, data = nbm.get_notebook(notebook_id, format)
625
625
626 if format == u'json':
626 if format == u'json':
627 self.set_header('Content-Type', 'application/json')
627 self.set_header('Content-Type', 'application/json')
628 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
628 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
629 elif format == u'py':
629 elif format == u'py':
630 self.set_header('Content-Type', 'application/x-python')
630 self.set_header('Content-Type', 'application/x-python')
631 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
631 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
632 self.set_header('Last-Modified', last_mod)
632 self.set_header('Last-Modified', last_mod)
633 self.finish(data)
633 self.finish(data)
634
634
635 @web.authenticated
635 @web.authenticated
636 def put(self, notebook_id):
636 def put(self, notebook_id):
637 nbm = self.application.notebook_manager
637 nbm = self.application.notebook_manager
638 format = self.get_argument('format', default='json')
638 format = self.get_argument('format', default='json')
639 name = self.get_argument('name', default=None)
639 name = self.get_argument('name', default=None)
640 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
640 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
641 self.set_status(204)
641 self.set_status(204)
642 self.finish()
642 self.finish()
643
643
644 @web.authenticated
644 @web.authenticated
645 def delete(self, notebook_id):
645 def delete(self, notebook_id):
646 nbm = self.application.notebook_manager
646 nbm = self.application.notebook_manager
647 nbm.delete_notebook(notebook_id)
647 nbm.delete_notebook(notebook_id)
648 self.set_status(204)
648 self.set_status(204)
649 self.finish()
649 self.finish()
650
650
651
651
652 class NotebookCopyHandler(AuthenticatedHandler):
652 class NotebookCopyHandler(AuthenticatedHandler):
653
653
654 @web.authenticated
654 @web.authenticated
655 def get(self, notebook_id):
655 def get(self, notebook_id):
656 nbm = self.application.notebook_manager
656 nbm = self.application.notebook_manager
657 project = nbm.notebook_dir
657 project = nbm.notebook_dir
658 notebook_id = nbm.copy_notebook(notebook_id)
658 notebook_id = nbm.copy_notebook(notebook_id)
659 self.render(
659 self.render(
660 'notebook.html', project=project,
660 'notebook.html', project=project,
661 notebook_id=notebook_id,
661 notebook_id=notebook_id,
662 base_project_url=self.application.ipython_app.base_project_url,
662 base_project_url=self.application.ipython_app.base_project_url,
663 base_kernel_url=self.application.ipython_app.base_kernel_url,
663 base_kernel_url=self.application.ipython_app.base_kernel_url,
664 kill_kernel=False,
664 kill_kernel=False,
665 read_only=False,
665 read_only=False,
666 logged_in=self.logged_in,
666 logged_in=self.logged_in,
667 login_available=self.login_available,
667 login_available=self.login_available,
668 mathjax_url=self.application.ipython_app.mathjax_url,
668 mathjax_url=self.application.ipython_app.mathjax_url,
669 )
669 )
670
670
671
671
672 #-----------------------------------------------------------------------------
672 #-----------------------------------------------------------------------------
673 # Cluster handlers
673 # Cluster handlers
674 #-----------------------------------------------------------------------------
674 #-----------------------------------------------------------------------------
675
675
676
676
677 class MainClusterHandler(AuthenticatedHandler):
677 class MainClusterHandler(AuthenticatedHandler):
678
678
679 @web.authenticated
679 @web.authenticated
680 def get(self):
680 def get(self):
681 cm = self.application.cluster_manager
681 cm = self.application.cluster_manager
682 self.finish(jsonapi.dumps(cm.list_profiles()))
682 self.finish(jsonapi.dumps(cm.list_profiles()))
683
683
684
684
685 class ClusterProfileHandler(AuthenticatedHandler):
685 class ClusterProfileHandler(AuthenticatedHandler):
686
686
687 @web.authenticated
687 @web.authenticated
688 def get(self, profile):
688 def get(self, profile):
689 cm = self.application.cluster_manager
689 cm = self.application.cluster_manager
690 self.finish(jsonapi.dumps(cm.profile_info(profile)))
690 self.finish(jsonapi.dumps(cm.profile_info(profile)))
691
691
692
692
693 class ClusterActionHandler(AuthenticatedHandler):
693 class ClusterActionHandler(AuthenticatedHandler):
694
694
695 @web.authenticated
695 @web.authenticated
696 def post(self, profile, action):
696 def post(self, profile, action):
697 cm = self.application.cluster_manager
697 cm = self.application.cluster_manager
698 if action == 'start':
698 if action == 'start':
699 n = self.get_argument('n',default=None)
699 n = self.get_argument('n',default=None)
700 if n is None:
700 if n is None:
701 data = cm.start_cluster(profile)
701 data = cm.start_cluster(profile)
702 else:
702 else:
703 data = cm.start_cluster(profile,int(n))
703 data = cm.start_cluster(profile,int(n))
704 if action == 'stop':
704 if action == 'stop':
705 data = cm.stop_cluster(profile)
705 data = cm.stop_cluster(profile)
706 self.finish(jsonapi.dumps(data))
706 self.finish(jsonapi.dumps(data))
707
707
708
708
709 #-----------------------------------------------------------------------------
709 #-----------------------------------------------------------------------------
710 # RST web service handlers
710 # RST web service handlers
711 #-----------------------------------------------------------------------------
711 #-----------------------------------------------------------------------------
712
712
713
713
714 class RSTHandler(AuthenticatedHandler):
714 class RSTHandler(AuthenticatedHandler):
715
715
716 @web.authenticated
716 @web.authenticated
717 def post(self):
717 def post(self):
718 if publish_string is None:
718 if publish_string is None:
719 raise web.HTTPError(503, u'docutils not available')
719 raise web.HTTPError(503, u'docutils not available')
720 body = self.request.body.strip()
720 body = self.request.body.strip()
721 source = body
721 source = body
722 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
722 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
723 defaults = {'file_insertion_enabled': 0,
723 defaults = {'file_insertion_enabled': 0,
724 'raw_enabled': 0,
724 'raw_enabled': 0,
725 '_disable_config': 1,
725 '_disable_config': 1,
726 'stylesheet_path': 0
726 'stylesheet_path': 0
727 # 'template': template_path
727 # 'template': template_path
728 }
728 }
729 try:
729 try:
730 html = publish_string(source, writer_name='html',
730 html = publish_string(source, writer_name='html',
731 settings_overrides=defaults
731 settings_overrides=defaults
732 )
732 )
733 except:
733 except:
734 raise web.HTTPError(400, u'Invalid RST')
734 raise web.HTTPError(400, u'Invalid RST')
735 print html
735 print html
736 self.set_header('Content-Type', 'text/html')
736 self.set_header('Content-Type', 'text/html')
737 self.finish(html)
737 self.finish(html)
738
738
739
739
@@ -1,310 +1,310 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 // NotebookList
9 // NotebookList
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
13
14 var NotebookList = function (selector) {
14 var NotebookList = 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 NotebookList.prototype.style = function () {
23 NotebookList.prototype.style = function () {
24 $('#notebook_toolbar').addClass('list_toolbar');
24 $('#notebook_toolbar').addClass('list_toolbar');
25 $('#drag_info').addClass('toolbar_info');
25 $('#drag_info').addClass('toolbar_info');
26 $('#notebook_buttons').addClass('toolbar_buttons');
26 $('#notebook_buttons').addClass('toolbar_buttons');
27 $('div#project_name').addClass('list_header ui-widget ui-widget-header');
27 $('div#project_name').addClass('list_header ui-widget ui-widget-header');
28 $('#refresh_notebook_list').button({
28 $('#refresh_notebook_list').button({
29 icons : {primary: 'ui-icon-arrowrefresh-1-s'},
29 icons : {primary: 'ui-icon-arrowrefresh-1-s'},
30 text : false
30 text : false
31 });
31 });
32 };
32 };
33
33
34
34
35 NotebookList.prototype.bind_events = function () {
35 NotebookList.prototype.bind_events = function () {
36 if (IPython.read_only){
36 if (IPython.read_only){
37 return;
37 return;
38 }
38 }
39 var that = this;
39 var that = this;
40 $('#refresh_notebook_list').click(function () {
40 $('#refresh_notebook_list').click(function () {
41 that.load_list();
41 that.load_list();
42 });
42 });
43 this.element.bind('dragover', function () {
43 this.element.bind('dragover', function () {
44 return false;
44 return false;
45 });
45 });
46 this.element.bind('drop', function(event){
46 this.element.bind('drop', function(event){
47 console.log('bound to drop');
47 console.log('bound to drop');
48 that.handelFilesUpload(event,'drop');
48 that.handelFilesUpload(event,'drop');
49 return false;
49 return false;
50 });
50 });
51 };
51 };
52
52
53 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
53 NotebookList.prototype.handelFilesUpload = function(event, dropOrForm) {
54 var that = this;
54 var that = this;
55 var files;
55 var files;
56 if(dropOrForm =='drop'){
56 if(dropOrForm =='drop'){
57 files = event.originalEvent.dataTransfer.files;
57 files = event.originalEvent.dataTransfer.files;
58 } else
58 } else
59 {
59 {
60 files = event.originalEvent.target.files
60 files = event.originalEvent.target.files
61 }
61 }
62 for (var i = 0, f; f = files[i]; i++) {
62 for (var i = 0, f; f = files[i]; i++) {
63 var reader = new FileReader();
63 var reader = new FileReader();
64 reader.readAsText(f);
64 reader.readAsText(f);
65 var fname = f.name.split('.');
65 var fname = f.name.split('.');
66 var nbname = fname.slice(0,-1).join('.');
66 var nbname = fname.slice(0,-1).join('.');
67 var nbformat = fname.slice(-1)[0];
67 var nbformat = fname.slice(-1)[0];
68 if (nbformat === 'ipynb') {nbformat = 'json';};
68 if (nbformat === 'ipynb') {nbformat = 'json';};
69 if (nbformat === 'py' || nbformat === 'json') {
69 if (nbformat === 'py' || nbformat === 'json') {
70 var item = that.new_notebook_item(0);
70 var item = that.new_notebook_item(0);
71 that.add_name_input(nbname, item);
71 that.add_name_input(nbname, item);
72 item.data('nbformat', nbformat);
72 item.data('nbformat', nbformat);
73 // Store the notebook item in the reader so we can use it later
73 // Store the notebook item in the reader so we can use it later
74 // to know which item it belongs to.
74 // to know which item it belongs to.
75 $(reader).data('item', item);
75 $(reader).data('item', item);
76 reader.onload = function (event) {
76 reader.onload = function (event) {
77 var nbitem = $(event.target).data('item');
77 var nbitem = $(event.target).data('item');
78 that.add_notebook_data(event.target.result, nbitem);
78 that.add_notebook_data(event.target.result, nbitem);
79 that.add_upload_button(nbitem);
79 that.add_upload_button(nbitem);
80 };
80 };
81 };
81 };
82 }
82 }
83 return false;
83 return false;
84 };
84 };
85
85
86 NotebookList.prototype.clear_list = function () {
86 NotebookList.prototype.clear_list = function () {
87 this.element.children('.list_item').remove();
87 this.element.children('.list_item').remove();
88 }
88 }
89
89
90
90
91 NotebookList.prototype.load_list = function () {
91 NotebookList.prototype.load_list = function () {
92 var settings = {
92 var settings = {
93 processData : false,
93 processData : false,
94 cache : false,
94 cache : false,
95 type : "GET",
95 type : "GET",
96 dataType : "json",
96 dataType : "json",
97 success : $.proxy(this.list_loaded, this)
97 success : $.proxy(this.list_loaded, this)
98 };
98 };
99 var url = $('body').data('baseProjectUrl') + 'notebooks';
99 var url = $('body').data('baseProjectUrl') + 'notebooks';
100 $.ajax(url, settings);
100 $.ajax(url, settings);
101 };
101 };
102
102
103
103
104 NotebookList.prototype.list_loaded = function (data, status, xhr) {
104 NotebookList.prototype.list_loaded = function (data, status, xhr) {
105 var len = data.length;
105 var len = data.length;
106 this.clear_list();
106 this.clear_list();
107 // Todo: remove old children
107 // Todo: remove old children
108 for (var i=0; i<len; i++) {
108 for (var i=0; i<len; i++) {
109 var notebook_id = data[i].notebook_id;
109 var notebook_id = data[i].notebook_id;
110 var nbname = data[i].name;
110 var nbname = data[i].name;
111 var kernel = data[i].kernel_status;
111 var kernel = data[i].kernel_id;
112 var item = this.new_notebook_item(i);
112 var item = this.new_notebook_item(i);
113 this.add_link(notebook_id, nbname, item);
113 this.add_link(notebook_id, nbname, item);
114 if (!IPython.read_only){
114 if (!IPython.read_only){
115 // hide delete buttons when readonly
115 // hide delete buttons when readonly
116 if(kernel == undefined){
116 if(kernel == undefined){
117 this.add_delete_button(item);
117 this.add_delete_button(item);
118 } else {
118 } else {
119 this.add_shutdown_button(item,kernel);
119 this.add_shutdown_button(item,kernel);
120 }
120 }
121 }
121 }
122 };
122 };
123 };
123 };
124
124
125
125
126 NotebookList.prototype.new_notebook_item = function (index) {
126 NotebookList.prototype.new_notebook_item = function (index) {
127 var item = $('<div/>');
127 var item = $('<div/>');
128 item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
128 item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
129 item.css('border-top-style','none');
129 item.css('border-top-style','none');
130 var item_name = $('<span/>').addClass('item_name');
130 var item_name = $('<span/>').addClass('item_name');
131
131
132 item.append(item_name);
132 item.append(item_name);
133 if (index === -1) {
133 if (index === -1) {
134 this.element.append(item);
134 this.element.append(item);
135 } else {
135 } else {
136 this.element.children().eq(index).after(item);
136 this.element.children().eq(index).after(item);
137 }
137 }
138 return item;
138 return item;
139 };
139 };
140
140
141
141
142 NotebookList.prototype.add_link = function (notebook_id, nbname, item) {
142 NotebookList.prototype.add_link = function (notebook_id, nbname, item) {
143 item.data('nbname', nbname);
143 item.data('nbname', nbname);
144 item.data('notebook_id', notebook_id);
144 item.data('notebook_id', notebook_id);
145 var new_item_name = $('<span/>').addClass('item_name');
145 var new_item_name = $('<span/>').addClass('item_name');
146 new_item_name.append(
146 new_item_name.append(
147 $('<a/>').
147 $('<a/>').
148 attr('href', $('body').data('baseProjectUrl')+notebook_id).
148 attr('href', $('body').data('baseProjectUrl')+notebook_id).
149 attr('target','_blank').
149 attr('target','_blank').
150 text(nbname)
150 text(nbname)
151 );
151 );
152 var e = item.find('.item_name');
152 var e = item.find('.item_name');
153 if (e.length === 0) {
153 if (e.length === 0) {
154 item.append(new_item_name);
154 item.append(new_item_name);
155 } else {
155 } else {
156 e.replaceWith(new_item_name);
156 e.replaceWith(new_item_name);
157 };
157 };
158 };
158 };
159
159
160
160
161 NotebookList.prototype.add_name_input = function (nbname, item) {
161 NotebookList.prototype.add_name_input = function (nbname, item) {
162 item.data('nbname', nbname);
162 item.data('nbname', nbname);
163 var new_item_name = $('<span/>').addClass('item_name');
163 var new_item_name = $('<span/>').addClass('item_name');
164 new_item_name.append(
164 new_item_name.append(
165 $('<input/>').addClass('ui-widget ui-widget-content').
165 $('<input/>').addClass('ui-widget ui-widget-content').
166 attr('value', nbname).
166 attr('value', nbname).
167 attr('size', '30').
167 attr('size', '30').
168 attr('type', 'text')
168 attr('type', 'text')
169 );
169 );
170 var e = item.find('.item_name');
170 var e = item.find('.item_name');
171 if (e.length === 0) {
171 if (e.length === 0) {
172 item.append(new_item_name);
172 item.append(new_item_name);
173 } else {
173 } else {
174 e.replaceWith(new_item_name);
174 e.replaceWith(new_item_name);
175 };
175 };
176 };
176 };
177
177
178
178
179 NotebookList.prototype.add_notebook_data = function (data, item) {
179 NotebookList.prototype.add_notebook_data = function (data, item) {
180 item.data('nbdata',data);
180 item.data('nbdata',data);
181 };
181 };
182
182
183
183
184 NotebookList.prototype.add_shutdown_button = function (item,kernel) {
184 NotebookList.prototype.add_shutdown_button = function (item,kernel) {
185 var new_buttons = $('<span/>').addClass('item_buttons');
185 var new_buttons = $('<span/>').addClass('item_buttons');
186 var that = this;
186 var that = this;
187 var shutdown_button = $('<button>Shutdown</button>').button().
187 var shutdown_button = $('<button>Shutdown</button>').button().
188 click(function (e) {
188 click(function (e) {
189 var settings = {
189 var settings = {
190 processData : false,
190 processData : false,
191 cache : false,
191 cache : false,
192 type : "DELETE",
192 type : "DELETE",
193 dataType : "json",
193 dataType : "json",
194 success : function (data, status, xhr) {
194 success : function (data, status, xhr) {
195 that.load_list();
195 that.load_list();
196 }
196 }
197 };
197 };
198 var url = $('body').data('baseProjectUrl') + 'kernels/'+kernel;
198 var url = $('body').data('baseProjectUrl') + 'kernels/'+kernel;
199 $.ajax(url, settings);
199 $.ajax(url, settings);
200 });
200 });
201 new_buttons.append(shutdown_button);
201 new_buttons.append(shutdown_button);
202 var e = item.find('.item_buttons');
202 var e = item.find('.item_buttons');
203 if (e.length === 0) {
203 if (e.length === 0) {
204 item.append(new_buttons);
204 item.append(new_buttons);
205 } else {
205 } else {
206 e.replaceWith(new_buttons);
206 e.replaceWith(new_buttons);
207 };
207 };
208 };
208 };
209
209
210 NotebookList.prototype.add_delete_button = function (item) {
210 NotebookList.prototype.add_delete_button = function (item) {
211 var new_buttons = $('<span/>').addClass('item_buttons');
211 var new_buttons = $('<span/>').addClass('item_buttons');
212 var delete_button = $('<button>Delete</button>').button().
212 var delete_button = $('<button>Delete</button>').button().
213 click(function (e) {
213 click(function (e) {
214 // $(this) is the button that was clicked.
214 // $(this) is the button that was clicked.
215 var that = $(this);
215 var that = $(this);
216 // We use the nbname and notebook_id from the parent notebook_item element's
216 // We use the nbname and notebook_id from the parent notebook_item element's
217 // data because the outer scopes values change as we iterate through the loop.
217 // data because the outer scopes values change as we iterate through the loop.
218 var parent_item = that.parents('div.list_item');
218 var parent_item = that.parents('div.list_item');
219 var nbname = parent_item.data('nbname');
219 var nbname = parent_item.data('nbname');
220 var notebook_id = parent_item.data('notebook_id');
220 var notebook_id = parent_item.data('notebook_id');
221 var dialog = $('<div/>');
221 var dialog = $('<div/>');
222 dialog.html('Are you sure you want to permanently delete the notebook: ' + nbname + '?');
222 dialog.html('Are you sure you want to permanently delete the notebook: ' + nbname + '?');
223 parent_item.append(dialog);
223 parent_item.append(dialog);
224 dialog.dialog({
224 dialog.dialog({
225 resizable: false,
225 resizable: false,
226 modal: true,
226 modal: true,
227 title: "Delete notebook",
227 title: "Delete notebook",
228 buttons : {
228 buttons : {
229 "Delete": function () {
229 "Delete": function () {
230 var settings = {
230 var settings = {
231 processData : false,
231 processData : false,
232 cache : false,
232 cache : false,
233 type : "DELETE",
233 type : "DELETE",
234 dataType : "json",
234 dataType : "json",
235 success : function (data, status, xhr) {
235 success : function (data, status, xhr) {
236 parent_item.remove();
236 parent_item.remove();
237 }
237 }
238 };
238 };
239 var url = $('body').data('baseProjectUrl') + 'notebooks/' + notebook_id;
239 var url = $('body').data('baseProjectUrl') + 'notebooks/' + notebook_id;
240 $.ajax(url, settings);
240 $.ajax(url, settings);
241 $(this).dialog('close');
241 $(this).dialog('close');
242 },
242 },
243 "Cancel": function () {
243 "Cancel": function () {
244 $(this).dialog('close');
244 $(this).dialog('close');
245 }
245 }
246 }
246 }
247 });
247 });
248 });
248 });
249 new_buttons.append(delete_button);
249 new_buttons.append(delete_button);
250 var e = item.find('.item_buttons');
250 var e = item.find('.item_buttons');
251 if (e.length === 0) {
251 if (e.length === 0) {
252 item.append(new_buttons);
252 item.append(new_buttons);
253 } else {
253 } else {
254 e.replaceWith(new_buttons);
254 e.replaceWith(new_buttons);
255 };
255 };
256 };
256 };
257
257
258
258
259 NotebookList.prototype.add_upload_button = function (item) {
259 NotebookList.prototype.add_upload_button = function (item) {
260 var that = this;
260 var that = this;
261 var new_buttons = $('<span/>').addClass('item_buttons');
261 var new_buttons = $('<span/>').addClass('item_buttons');
262 var upload_button = $('<button>Upload</button>').button().
262 var upload_button = $('<button>Upload</button>').button().
263 click(function (e) {
263 click(function (e) {
264 var nbname = item.find('.item_name > input').attr('value');
264 var nbname = item.find('.item_name > input').attr('value');
265 var nbformat = item.data('nbformat');
265 var nbformat = item.data('nbformat');
266 var nbdata = item.data('nbdata');
266 var nbdata = item.data('nbdata');
267 var content_type = 'text/plain';
267 var content_type = 'text/plain';
268 if (nbformat === 'json') {
268 if (nbformat === 'json') {
269 content_type = 'application/json';
269 content_type = 'application/json';
270 } else if (nbformat === 'py') {
270 } else if (nbformat === 'py') {
271 content_type = 'application/x-python';
271 content_type = 'application/x-python';
272 };
272 };
273 var settings = {
273 var settings = {
274 processData : false,
274 processData : false,
275 cache : false,
275 cache : false,
276 type : 'POST',
276 type : 'POST',
277 dataType : 'json',
277 dataType : 'json',
278 data : nbdata,
278 data : nbdata,
279 headers : {'Content-Type': content_type},
279 headers : {'Content-Type': content_type},
280 success : function (data, status, xhr) {
280 success : function (data, status, xhr) {
281 that.add_link(data, nbname, item);
281 that.add_link(data, nbname, item);
282 that.add_delete_button(item);
282 that.add_delete_button(item);
283 }
283 }
284 };
284 };
285
285
286 var qs = $.param({name:nbname, format:nbformat});
286 var qs = $.param({name:nbname, format:nbformat});
287 var url = $('body').data('baseProjectUrl') + 'notebooks?' + qs;
287 var url = $('body').data('baseProjectUrl') + 'notebooks?' + qs;
288 $.ajax(url, settings);
288 $.ajax(url, settings);
289 });
289 });
290 var cancel_button = $('<button>Cancel</button>').button().
290 var cancel_button = $('<button>Cancel</button>').button().
291 click(function (e) {
291 click(function (e) {
292 item.remove();
292 item.remove();
293 });
293 });
294 upload_button.addClass('upload_button');
294 upload_button.addClass('upload_button');
295 new_buttons.append(upload_button).append(cancel_button);
295 new_buttons.append(upload_button).append(cancel_button);
296 var e = item.find('.item_buttons');
296 var e = item.find('.item_buttons');
297 if (e.length === 0) {
297 if (e.length === 0) {
298 item.append(new_buttons);
298 item.append(new_buttons);
299 } else {
299 } else {
300 e.replaceWith(new_buttons);
300 e.replaceWith(new_buttons);
301 };
301 };
302 };
302 };
303
303
304
304
305 IPython.NotebookList = NotebookList;
305 IPython.NotebookList = NotebookList;
306
306
307 return IPython;
307 return IPython;
308
308
309 }(IPython));
309 }(IPython));
310
310
General Comments 0
You need to be logged in to leave comments. Login now