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