##// END OF EJS Templates
rename kernel_status -> kernel_id
Matthias BUSSONNIER -
Show More
@@ -1,739 +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 f['kernel_status']=kid
599 f['kernel_id']=kid
600 600 self.finish(jsonapi.dumps(files))
601 601
602 602 @web.authenticated
603 603 def post(self):
604 604 nbm = self.application.notebook_manager
605 605 body = self.request.body.strip()
606 606 format = self.get_argument('format', default='json')
607 607 name = self.get_argument('name', default=None)
608 608 if body:
609 609 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
610 610 else:
611 611 notebook_id = nbm.new_notebook()
612 612 self.set_header('Location', '/'+notebook_id)
613 613 self.finish(jsonapi.dumps(notebook_id))
614 614
615 615
616 616 class NotebookHandler(AuthenticatedHandler):
617 617
618 618 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
619 619
620 620 @authenticate_unless_readonly
621 621 def get(self, notebook_id):
622 622 nbm = self.application.notebook_manager
623 623 format = self.get_argument('format', default='json')
624 624 last_mod, name, data = nbm.get_notebook(notebook_id, format)
625 625
626 626 if format == u'json':
627 627 self.set_header('Content-Type', 'application/json')
628 628 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
629 629 elif format == u'py':
630 630 self.set_header('Content-Type', 'application/x-python')
631 631 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
632 632 self.set_header('Last-Modified', last_mod)
633 633 self.finish(data)
634 634
635 635 @web.authenticated
636 636 def put(self, notebook_id):
637 637 nbm = self.application.notebook_manager
638 638 format = self.get_argument('format', default='json')
639 639 name = self.get_argument('name', default=None)
640 640 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
641 641 self.set_status(204)
642 642 self.finish()
643 643
644 644 @web.authenticated
645 645 def delete(self, notebook_id):
646 646 nbm = self.application.notebook_manager
647 647 nbm.delete_notebook(notebook_id)
648 648 self.set_status(204)
649 649 self.finish()
650 650
651 651
652 652 class NotebookCopyHandler(AuthenticatedHandler):
653 653
654 654 @web.authenticated
655 655 def get(self, notebook_id):
656 656 nbm = self.application.notebook_manager
657 657 project = nbm.notebook_dir
658 658 notebook_id = nbm.copy_notebook(notebook_id)
659 659 self.render(
660 660 'notebook.html', project=project,
661 661 notebook_id=notebook_id,
662 662 base_project_url=self.application.ipython_app.base_project_url,
663 663 base_kernel_url=self.application.ipython_app.base_kernel_url,
664 664 kill_kernel=False,
665 665 read_only=False,
666 666 logged_in=self.logged_in,
667 667 login_available=self.login_available,
668 668 mathjax_url=self.application.ipython_app.mathjax_url,
669 669 )
670 670
671 671
672 672 #-----------------------------------------------------------------------------
673 673 # Cluster handlers
674 674 #-----------------------------------------------------------------------------
675 675
676 676
677 677 class MainClusterHandler(AuthenticatedHandler):
678 678
679 679 @web.authenticated
680 680 def get(self):
681 681 cm = self.application.cluster_manager
682 682 self.finish(jsonapi.dumps(cm.list_profiles()))
683 683
684 684
685 685 class ClusterProfileHandler(AuthenticatedHandler):
686 686
687 687 @web.authenticated
688 688 def get(self, profile):
689 689 cm = self.application.cluster_manager
690 690 self.finish(jsonapi.dumps(cm.profile_info(profile)))
691 691
692 692
693 693 class ClusterActionHandler(AuthenticatedHandler):
694 694
695 695 @web.authenticated
696 696 def post(self, profile, action):
697 697 cm = self.application.cluster_manager
698 698 if action == 'start':
699 699 n = self.get_argument('n',default=None)
700 700 if n is None:
701 701 data = cm.start_cluster(profile)
702 702 else:
703 703 data = cm.start_cluster(profile,int(n))
704 704 if action == 'stop':
705 705 data = cm.stop_cluster(profile)
706 706 self.finish(jsonapi.dumps(data))
707 707
708 708
709 709 #-----------------------------------------------------------------------------
710 710 # RST web service handlers
711 711 #-----------------------------------------------------------------------------
712 712
713 713
714 714 class RSTHandler(AuthenticatedHandler):
715 715
716 716 @web.authenticated
717 717 def post(self):
718 718 if publish_string is None:
719 719 raise web.HTTPError(503, u'docutils not available')
720 720 body = self.request.body.strip()
721 721 source = body
722 722 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
723 723 defaults = {'file_insertion_enabled': 0,
724 724 'raw_enabled': 0,
725 725 '_disable_config': 1,
726 726 'stylesheet_path': 0
727 727 # 'template': template_path
728 728 }
729 729 try:
730 730 html = publish_string(source, writer_name='html',
731 731 settings_overrides=defaults
732 732 )
733 733 except:
734 734 raise web.HTTPError(400, u'Invalid RST')
735 735 print html
736 736 self.set_header('Content-Type', 'text/html')
737 737 self.finish(html)
738 738
739 739
@@ -1,310 +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 var kernel = data[i].kernel_status;
111 var kernel = data[i].kernel_id;
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 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 195 that.load_list();
196 196 }
197 197 };
198 198 var url = $('body').data('baseProjectUrl') + 'kernels/'+kernel;
199 199 $.ajax(url, settings);
200 200 });
201 201 new_buttons.append(shutdown_button);
202 202 var e = item.find('.item_buttons');
203 203 if (e.length === 0) {
204 204 item.append(new_buttons);
205 205 } else {
206 206 e.replaceWith(new_buttons);
207 207 };
208 208 };
209 209
210 210 NotebookList.prototype.add_delete_button = function (item) {
211 211 var new_buttons = $('<span/>').addClass('item_buttons');
212 212 var delete_button = $('<button>Delete</button>').button().
213 213 click(function (e) {
214 214 // $(this) is the button that was clicked.
215 215 var that = $(this);
216 216 // We use the nbname and notebook_id from the parent notebook_item element's
217 217 // data because the outer scopes values change as we iterate through the loop.
218 218 var parent_item = that.parents('div.list_item');
219 219 var nbname = parent_item.data('nbname');
220 220 var notebook_id = parent_item.data('notebook_id');
221 221 var dialog = $('<div/>');
222 222 dialog.html('Are you sure you want to permanently delete the notebook: ' + nbname + '?');
223 223 parent_item.append(dialog);
224 224 dialog.dialog({
225 225 resizable: false,
226 226 modal: true,
227 227 title: "Delete notebook",
228 228 buttons : {
229 229 "Delete": function () {
230 230 var settings = {
231 231 processData : false,
232 232 cache : false,
233 233 type : "DELETE",
234 234 dataType : "json",
235 235 success : function (data, status, xhr) {
236 236 parent_item.remove();
237 237 }
238 238 };
239 239 var url = $('body').data('baseProjectUrl') + 'notebooks/' + notebook_id;
240 240 $.ajax(url, settings);
241 241 $(this).dialog('close');
242 242 },
243 243 "Cancel": function () {
244 244 $(this).dialog('close');
245 245 }
246 246 }
247 247 });
248 248 });
249 249 new_buttons.append(delete_button);
250 250 var e = item.find('.item_buttons');
251 251 if (e.length === 0) {
252 252 item.append(new_buttons);
253 253 } else {
254 254 e.replaceWith(new_buttons);
255 255 };
256 256 };
257 257
258 258
259 259 NotebookList.prototype.add_upload_button = function (item) {
260 260 var that = this;
261 261 var new_buttons = $('<span/>').addClass('item_buttons');
262 262 var upload_button = $('<button>Upload</button>').button().
263 263 click(function (e) {
264 264 var nbname = item.find('.item_name > input').attr('value');
265 265 var nbformat = item.data('nbformat');
266 266 var nbdata = item.data('nbdata');
267 267 var content_type = 'text/plain';
268 268 if (nbformat === 'json') {
269 269 content_type = 'application/json';
270 270 } else if (nbformat === 'py') {
271 271 content_type = 'application/x-python';
272 272 };
273 273 var settings = {
274 274 processData : false,
275 275 cache : false,
276 276 type : 'POST',
277 277 dataType : 'json',
278 278 data : nbdata,
279 279 headers : {'Content-Type': content_type},
280 280 success : function (data, status, xhr) {
281 281 that.add_link(data, nbname, item);
282 282 that.add_delete_button(item);
283 283 }
284 284 };
285 285
286 286 var qs = $.param({name:nbname, format:nbformat});
287 287 var url = $('body').data('baseProjectUrl') + 'notebooks?' + qs;
288 288 $.ajax(url, settings);
289 289 });
290 290 var cancel_button = $('<button>Cancel</button>').button().
291 291 click(function (e) {
292 292 item.remove();
293 293 });
294 294 upload_button.addClass('upload_button');
295 295 new_buttons.append(upload_button).append(cancel_button);
296 296 var e = item.find('.item_buttons');
297 297 if (e.length === 0) {
298 298 item.append(new_buttons);
299 299 } else {
300 300 e.replaceWith(new_buttons);
301 301 };
302 302 };
303 303
304 304
305 305 IPython.NotebookList = NotebookList;
306 306
307 307 return IPython;
308 308
309 309 }(IPython));
310 310
General Comments 0
You need to be logged in to leave comments. Login now