##// END OF EJS Templates
Merge pull request #2218 from Carreau/new_redirect...
Brian E. Granger -
r8189:1ad91e6b merge
parent child Browse files
Show More
@@ -1,920 +1,908 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 Cookie
20 20 import datetime
21 21 import email.utils
22 22 import hashlib
23 23 import logging
24 24 import mimetypes
25 25 import os
26 26 import stat
27 27 import threading
28 28 import time
29 29 import uuid
30 30
31 31 from tornado import web
32 32 from tornado import websocket
33 33
34 34 from zmq.eventloop import ioloop
35 35 from zmq.utils import jsonapi
36 36
37 37 from IPython.external.decorator import decorator
38 38 from IPython.zmq.session import Session
39 39 from IPython.lib.security import passwd_check
40 40 from IPython.utils.jsonutil import date_default
41 41 from IPython.utils.path import filefind
42 42
43 43 try:
44 44 from docutils.core import publish_string
45 45 except ImportError:
46 46 publish_string = None
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
50 50 #-----------------------------------------------------------------------------
51 51
52 52 # Google Chrome, as of release 16, changed its websocket protocol number. The
53 53 # parts tornado cares about haven't really changed, so it's OK to continue
54 54 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
55 55 # version as of Oct 30/2011) the version check fails, see the issue report:
56 56
57 57 # https://github.com/facebook/tornado/issues/385
58 58
59 59 # This issue has been fixed in Tornado post 2.1.1:
60 60
61 61 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
62 62
63 63 # Here we manually apply the same patch as above so that users of IPython can
64 64 # continue to work with an officially released Tornado. We make the
65 65 # monkeypatch version check as narrow as possible to limit its effects; once
66 66 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
67 67
68 68 import tornado
69 69
70 70 if tornado.version_info <= (2,1,1):
71 71
72 72 def _execute(self, transforms, *args, **kwargs):
73 73 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
74 74
75 75 self.open_args = args
76 76 self.open_kwargs = kwargs
77 77
78 78 # The difference between version 8 and 13 is that in 8 the
79 79 # client sends a "Sec-Websocket-Origin" header and in 13 it's
80 80 # simply "Origin".
81 81 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
82 82 self.ws_connection = WebSocketProtocol8(self)
83 83 self.ws_connection.accept_connection()
84 84
85 85 elif self.request.headers.get("Sec-WebSocket-Version"):
86 86 self.stream.write(tornado.escape.utf8(
87 87 "HTTP/1.1 426 Upgrade Required\r\n"
88 88 "Sec-WebSocket-Version: 8\r\n\r\n"))
89 89 self.stream.close()
90 90
91 91 else:
92 92 self.ws_connection = WebSocketProtocol76(self)
93 93 self.ws_connection.accept_connection()
94 94
95 95 websocket.WebSocketHandler._execute = _execute
96 96 del _execute
97 97
98 98 #-----------------------------------------------------------------------------
99 99 # Decorator for disabling read-only handlers
100 100 #-----------------------------------------------------------------------------
101 101
102 102 @decorator
103 103 def not_if_readonly(f, self, *args, **kwargs):
104 104 if self.application.read_only:
105 105 raise web.HTTPError(403, "Notebook server is read-only")
106 106 else:
107 107 return f(self, *args, **kwargs)
108 108
109 109 @decorator
110 110 def authenticate_unless_readonly(f, self, *args, **kwargs):
111 111 """authenticate this page *unless* readonly view is active.
112 112
113 113 In read-only mode, the notebook list and print view should
114 114 be accessible without authentication.
115 115 """
116 116
117 117 @web.authenticated
118 118 def auth_f(self, *args, **kwargs):
119 119 return f(self, *args, **kwargs)
120 120
121 121 if self.application.read_only:
122 122 return f(self, *args, **kwargs)
123 123 else:
124 124 return auth_f(self, *args, **kwargs)
125 125
126 def urljoin(*pieces):
127 """Join componenet of url into a relative url
128
129 Use to prevent double slash when joining subpath
130 """
131 striped = [s.strip('/') for s in pieces]
132 return '/'.join(s for s in striped if s)
133
126 134 #-----------------------------------------------------------------------------
127 135 # Top-level handlers
128 136 #-----------------------------------------------------------------------------
129 137
130 138 class RequestHandler(web.RequestHandler):
131 139 """RequestHandler with default variable setting."""
132 140
133 141 def render(*args, **kwargs):
134 142 kwargs.setdefault('message', '')
135 143 return web.RequestHandler.render(*args, **kwargs)
136 144
137 145 class AuthenticatedHandler(RequestHandler):
138 146 """A RequestHandler with an authenticated user."""
139 147
140 148 def get_current_user(self):
141 149 user_id = self.get_secure_cookie("username")
142 150 # For now the user_id should not return empty, but it could eventually
143 151 if user_id == '':
144 152 user_id = 'anonymous'
145 153 if user_id is None:
146 154 # prevent extra Invalid cookie sig warnings:
147 155 self.clear_cookie('username')
148 156 if not self.application.password and not self.application.read_only:
149 157 user_id = 'anonymous'
150 158 return user_id
151 159
152 160 @property
153 161 def logged_in(self):
154 162 """Is a user currently logged in?
155 163
156 164 """
157 165 user = self.get_current_user()
158 166 return (user and not user == 'anonymous')
159 167
160 168 @property
161 169 def login_available(self):
162 170 """May a user proceed to log in?
163 171
164 172 This returns True if login capability is available, irrespective of
165 173 whether the user is already logged in or not.
166 174
167 175 """
168 176 return bool(self.application.password)
169 177
170 178 @property
171 179 def read_only(self):
172 180 """Is the notebook read-only?
173 181
174 182 """
175 183 return self.application.read_only
176 184
177 185 @property
178 186 def ws_url(self):
179 187 """websocket url matching the current request
180 188
181 189 turns http[s]://host[:port] into
182 190 ws[s]://host[:port]
183 191 """
184 192 proto = self.request.protocol.replace('http', 'ws')
185 193 host = self.application.ipython_app.websocket_host # default to config value
186 194 if host == '':
187 195 host = self.request.host # get from request
188 196 return "%s://%s" % (proto, host)
189 197
190 198
191 199 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
192 200 """static files should only be accessible when logged in"""
193 201
194 202 @authenticate_unless_readonly
195 203 def get(self, path):
196 204 return web.StaticFileHandler.get(self, path)
197 205
198 206
199 207 class ProjectDashboardHandler(AuthenticatedHandler):
200 208
201 209 @authenticate_unless_readonly
202 210 def get(self):
203 211 nbm = self.application.notebook_manager
204 212 project = nbm.notebook_dir
205 213 self.render(
206 214 'projectdashboard.html', project=project,
207 215 base_project_url=self.application.ipython_app.base_project_url,
208 216 base_kernel_url=self.application.ipython_app.base_kernel_url,
209 217 read_only=self.read_only,
210 218 logged_in=self.logged_in,
211 219 login_available=self.login_available
212 220 )
213 221
214 222
215 223 class LoginHandler(AuthenticatedHandler):
216 224
217 225 def _render(self, message=None):
218 226 self.render('login.html',
219 227 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
220 228 read_only=self.read_only,
221 229 logged_in=self.logged_in,
222 230 login_available=self.login_available,
223 231 base_project_url=self.application.ipython_app.base_project_url,
224 232 message=message
225 233 )
226 234
227 235 def get(self):
228 236 if self.current_user:
229 237 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
230 238 else:
231 239 self._render()
232 240
233 241 def post(self):
234 242 pwd = self.get_argument('password', default=u'')
235 243 if self.application.password:
236 244 if passwd_check(self.application.password, pwd):
237 245 self.set_secure_cookie('username', str(uuid.uuid4()))
238 246 else:
239 247 self._render(message={'error': 'Invalid password'})
240 248 return
241 249
242 250 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
243 251
244 252
245 253 class LogoutHandler(AuthenticatedHandler):
246 254
247 255 def get(self):
248 256 self.clear_cookie('username')
249 257 if self.login_available:
250 258 message = {'info': 'Successfully logged out.'}
251 259 else:
252 260 message = {'warning': 'Cannot log out. Notebook authentication '
253 261 'is disabled.'}
254 262
255 263 self.render('logout.html',
256 264 read_only=self.read_only,
257 265 logged_in=self.logged_in,
258 266 login_available=self.login_available,
259 267 base_project_url=self.application.ipython_app.base_project_url,
260 268 message=message)
261 269
262 270
263 271 class NewHandler(AuthenticatedHandler):
264 272
265 273 @web.authenticated
266 274 def get(self):
267 275 nbm = self.application.notebook_manager
268 276 project = nbm.notebook_dir
269 277 notebook_id = nbm.new_notebook()
270 self.render(
271 'notebook.html', project=project,
272 notebook_id=notebook_id,
273 base_project_url=self.application.ipython_app.base_project_url,
274 base_kernel_url=self.application.ipython_app.base_kernel_url,
275 kill_kernel=False,
276 read_only=False,
277 logged_in=self.logged_in,
278 login_available=self.login_available,
279 mathjax_url=self.application.ipython_app.mathjax_url,
280 )
278 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
281 279
282 280
283 281 class NamedNotebookHandler(AuthenticatedHandler):
284 282
285 283 @authenticate_unless_readonly
286 284 def get(self, notebook_id):
287 285 nbm = self.application.notebook_manager
288 286 project = nbm.notebook_dir
289 287 if not nbm.notebook_exists(notebook_id):
290 288 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
291 289
292 290 self.render(
293 291 'notebook.html', project=project,
294 292 notebook_id=notebook_id,
295 293 base_project_url=self.application.ipython_app.base_project_url,
296 294 base_kernel_url=self.application.ipython_app.base_kernel_url,
297 295 kill_kernel=False,
298 296 read_only=self.read_only,
299 297 logged_in=self.logged_in,
300 298 login_available=self.login_available,
301 299 mathjax_url=self.application.ipython_app.mathjax_url,
302 300 )
303 301
304 302
305 303 class PrintNotebookHandler(AuthenticatedHandler):
306 304
307 305 @authenticate_unless_readonly
308 306 def get(self, notebook_id):
309 307 nbm = self.application.notebook_manager
310 308 project = nbm.notebook_dir
311 309 if not nbm.notebook_exists(notebook_id):
312 310 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
313 311
314 312 self.render(
315 313 'printnotebook.html', project=project,
316 314 notebook_id=notebook_id,
317 315 base_project_url=self.application.ipython_app.base_project_url,
318 316 base_kernel_url=self.application.ipython_app.base_kernel_url,
319 317 kill_kernel=False,
320 318 read_only=self.read_only,
321 319 logged_in=self.logged_in,
322 320 login_available=self.login_available,
323 321 mathjax_url=self.application.ipython_app.mathjax_url,
324 322 )
325 323
326 324 #-----------------------------------------------------------------------------
327 325 # Kernel handlers
328 326 #-----------------------------------------------------------------------------
329 327
330 328
331 329 class MainKernelHandler(AuthenticatedHandler):
332 330
333 331 @web.authenticated
334 332 def get(self):
335 333 km = self.application.kernel_manager
336 334 self.finish(jsonapi.dumps(km.kernel_ids))
337 335
338 336 @web.authenticated
339 337 def post(self):
340 338 km = self.application.kernel_manager
341 339 nbm = self.application.notebook_manager
342 340 notebook_id = self.get_argument('notebook', default=None)
343 341 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
344 342 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
345 343 self.set_header('Location', '/'+kernel_id)
346 344 self.finish(jsonapi.dumps(data))
347 345
348 346
349 347 class KernelHandler(AuthenticatedHandler):
350 348
351 349 SUPPORTED_METHODS = ('DELETE')
352 350
353 351 @web.authenticated
354 352 def delete(self, kernel_id):
355 353 km = self.application.kernel_manager
356 354 km.shutdown_kernel(kernel_id)
357 355 self.set_status(204)
358 356 self.finish()
359 357
360 358
361 359 class KernelActionHandler(AuthenticatedHandler):
362 360
363 361 @web.authenticated
364 362 def post(self, kernel_id, action):
365 363 km = self.application.kernel_manager
366 364 if action == 'interrupt':
367 365 km.interrupt_kernel(kernel_id)
368 366 self.set_status(204)
369 367 if action == 'restart':
370 368 new_kernel_id = km.restart_kernel(kernel_id)
371 369 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
372 370 self.set_header('Location', '/'+new_kernel_id)
373 371 self.write(jsonapi.dumps(data))
374 372 self.finish()
375 373
376 374
377 375 class ZMQStreamHandler(websocket.WebSocketHandler):
378 376
379 377 def _reserialize_reply(self, msg_list):
380 378 """Reserialize a reply message using JSON.
381 379
382 380 This takes the msg list from the ZMQ socket, unserializes it using
383 381 self.session and then serializes the result using JSON. This method
384 382 should be used by self._on_zmq_reply to build messages that can
385 383 be sent back to the browser.
386 384 """
387 385 idents, msg_list = self.session.feed_identities(msg_list)
388 386 msg = self.session.unserialize(msg_list)
389 387 try:
390 388 msg['header'].pop('date')
391 389 except KeyError:
392 390 pass
393 391 try:
394 392 msg['parent_header'].pop('date')
395 393 except KeyError:
396 394 pass
397 395 msg.pop('buffers')
398 396 return jsonapi.dumps(msg, default=date_default)
399 397
400 398 def _on_zmq_reply(self, msg_list):
401 399 try:
402 400 msg = self._reserialize_reply(msg_list)
403 401 except Exception:
404 402 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
405 403 else:
406 404 self.write_message(msg)
407 405
408 406 def allow_draft76(self):
409 407 """Allow draft 76, until browsers such as Safari update to RFC 6455.
410 408
411 409 This has been disabled by default in tornado in release 2.2.0, and
412 410 support will be removed in later versions.
413 411 """
414 412 return True
415 413
416 414
417 415 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
418 416
419 417 def open(self, kernel_id):
420 418 self.kernel_id = kernel_id.decode('ascii')
421 419 try:
422 420 cfg = self.application.ipython_app.config
423 421 except AttributeError:
424 422 # protect from the case where this is run from something other than
425 423 # the notebook app:
426 424 cfg = None
427 425 self.session = Session(config=cfg)
428 426 self.save_on_message = self.on_message
429 427 self.on_message = self.on_first_message
430 428
431 429 def get_current_user(self):
432 430 user_id = self.get_secure_cookie("username")
433 431 if user_id == '' or (user_id is None and not self.application.password):
434 432 user_id = 'anonymous'
435 433 return user_id
436 434
437 435 def _inject_cookie_message(self, msg):
438 436 """Inject the first message, which is the document cookie,
439 437 for authentication."""
440 438 if isinstance(msg, unicode):
441 439 # Cookie can't constructor doesn't accept unicode strings for some reason
442 440 msg = msg.encode('utf8', 'replace')
443 441 try:
444 442 self.request._cookies = Cookie.SimpleCookie(msg)
445 443 except:
446 444 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
447 445
448 446 def on_first_message(self, msg):
449 447 self._inject_cookie_message(msg)
450 448 if self.get_current_user() is None:
451 449 logging.warn("Couldn't authenticate WebSocket connection")
452 450 raise web.HTTPError(403)
453 451 self.on_message = self.save_on_message
454 452
455 453
456 454 class IOPubHandler(AuthenticatedZMQStreamHandler):
457 455
458 456 def initialize(self, *args, **kwargs):
459 457 self._kernel_alive = True
460 458 self._beating = False
461 459 self.iopub_stream = None
462 460 self.hb_stream = None
463 461
464 462 def on_first_message(self, msg):
465 463 try:
466 464 super(IOPubHandler, self).on_first_message(msg)
467 465 except web.HTTPError:
468 466 self.close()
469 467 return
470 468 km = self.application.kernel_manager
471 469 self.time_to_dead = km.time_to_dead
472 470 self.first_beat = km.first_beat
473 471 kernel_id = self.kernel_id
474 472 try:
475 473 self.iopub_stream = km.create_iopub_stream(kernel_id)
476 474 self.hb_stream = km.create_hb_stream(kernel_id)
477 475 except web.HTTPError:
478 476 # WebSockets don't response to traditional error codes so we
479 477 # close the connection.
480 478 if not self.stream.closed():
481 479 self.stream.close()
482 480 self.close()
483 481 else:
484 482 self.iopub_stream.on_recv(self._on_zmq_reply)
485 483 self.start_hb(self.kernel_died)
486 484
487 485 def on_message(self, msg):
488 486 pass
489 487
490 488 def on_close(self):
491 489 # This method can be called twice, once by self.kernel_died and once
492 490 # from the WebSocket close event. If the WebSocket connection is
493 491 # closed before the ZMQ streams are setup, they could be None.
494 492 self.stop_hb()
495 493 if self.iopub_stream is not None and not self.iopub_stream.closed():
496 494 self.iopub_stream.on_recv(None)
497 495 self.iopub_stream.close()
498 496 if self.hb_stream is not None and not self.hb_stream.closed():
499 497 self.hb_stream.close()
500 498
501 499 def start_hb(self, callback):
502 500 """Start the heartbeating and call the callback if the kernel dies."""
503 501 if not self._beating:
504 502 self._kernel_alive = True
505 503
506 504 def ping_or_dead():
507 505 self.hb_stream.flush()
508 506 if self._kernel_alive:
509 507 self._kernel_alive = False
510 508 self.hb_stream.send(b'ping')
511 509 # flush stream to force immediate socket send
512 510 self.hb_stream.flush()
513 511 else:
514 512 try:
515 513 callback()
516 514 except:
517 515 pass
518 516 finally:
519 517 self.stop_hb()
520 518
521 519 def beat_received(msg):
522 520 self._kernel_alive = True
523 521
524 522 self.hb_stream.on_recv(beat_received)
525 523 loop = ioloop.IOLoop.instance()
526 524 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
527 525 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
528 526 self._beating= True
529 527
530 528 def _really_start_hb(self):
531 529 """callback for delayed heartbeat start
532 530
533 531 Only start the hb loop if we haven't been closed during the wait.
534 532 """
535 533 if self._beating and not self.hb_stream.closed():
536 534 self._hb_periodic_callback.start()
537 535
538 536 def stop_hb(self):
539 537 """Stop the heartbeating and cancel all related callbacks."""
540 538 if self._beating:
541 539 self._beating = False
542 540 self._hb_periodic_callback.stop()
543 541 if not self.hb_stream.closed():
544 542 self.hb_stream.on_recv(None)
545 543
546 544 def kernel_died(self):
547 545 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
548 546 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
549 547 self.write_message(
550 548 {'header': {'msg_type': 'status'},
551 549 'parent_header': {},
552 550 'content': {'execution_state':'dead'}
553 551 }
554 552 )
555 553 self.on_close()
556 554
557 555
558 556 class ShellHandler(AuthenticatedZMQStreamHandler):
559 557
560 558 def initialize(self, *args, **kwargs):
561 559 self.shell_stream = None
562 560
563 561 def on_first_message(self, msg):
564 562 try:
565 563 super(ShellHandler, self).on_first_message(msg)
566 564 except web.HTTPError:
567 565 self.close()
568 566 return
569 567 km = self.application.kernel_manager
570 568 self.max_msg_size = km.max_msg_size
571 569 kernel_id = self.kernel_id
572 570 try:
573 571 self.shell_stream = km.create_shell_stream(kernel_id)
574 572 except web.HTTPError:
575 573 # WebSockets don't response to traditional error codes so we
576 574 # close the connection.
577 575 if not self.stream.closed():
578 576 self.stream.close()
579 577 self.close()
580 578 else:
581 579 self.shell_stream.on_recv(self._on_zmq_reply)
582 580
583 581 def on_message(self, msg):
584 582 if len(msg) < self.max_msg_size:
585 583 msg = jsonapi.loads(msg)
586 584 self.session.send(self.shell_stream, msg)
587 585
588 586 def on_close(self):
589 587 # Make sure the stream exists and is not already closed.
590 588 if self.shell_stream is not None and not self.shell_stream.closed():
591 589 self.shell_stream.close()
592 590
593 591
594 592 #-----------------------------------------------------------------------------
595 593 # Notebook web service handlers
596 594 #-----------------------------------------------------------------------------
597 595
598 596 class NotebookRootHandler(AuthenticatedHandler):
599 597
600 598 @authenticate_unless_readonly
601 599 def get(self):
602 600 nbm = self.application.notebook_manager
603 601 km = self.application.kernel_manager
604 602 files = nbm.list_notebooks()
605 603 for f in files :
606 604 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
607 605 self.finish(jsonapi.dumps(files))
608 606
609 607 @web.authenticated
610 608 def post(self):
611 609 nbm = self.application.notebook_manager
612 610 body = self.request.body.strip()
613 611 format = self.get_argument('format', default='json')
614 612 name = self.get_argument('name', default=None)
615 613 if body:
616 614 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
617 615 else:
618 616 notebook_id = nbm.new_notebook()
619 617 self.set_header('Location', '/'+notebook_id)
620 618 self.finish(jsonapi.dumps(notebook_id))
621 619
622 620
623 621 class NotebookHandler(AuthenticatedHandler):
624 622
625 623 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
626 624
627 625 @authenticate_unless_readonly
628 626 def get(self, notebook_id):
629 627 nbm = self.application.notebook_manager
630 628 format = self.get_argument('format', default='json')
631 629 last_mod, name, data = nbm.get_notebook(notebook_id, format)
632 630
633 631 if format == u'json':
634 632 self.set_header('Content-Type', 'application/json')
635 633 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
636 634 elif format == u'py':
637 635 self.set_header('Content-Type', 'application/x-python')
638 636 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
639 637 self.set_header('Last-Modified', last_mod)
640 638 self.finish(data)
641 639
642 640 @web.authenticated
643 641 def put(self, notebook_id):
644 642 nbm = self.application.notebook_manager
645 643 format = self.get_argument('format', default='json')
646 644 name = self.get_argument('name', default=None)
647 645 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
648 646 self.set_status(204)
649 647 self.finish()
650 648
651 649 @web.authenticated
652 650 def delete(self, notebook_id):
653 651 nbm = self.application.notebook_manager
654 652 nbm.delete_notebook(notebook_id)
655 653 self.set_status(204)
656 654 self.finish()
657 655
658 656
659 657 class NotebookCopyHandler(AuthenticatedHandler):
660 658
661 659 @web.authenticated
662 660 def get(self, notebook_id):
663 661 nbm = self.application.notebook_manager
664 662 project = nbm.notebook_dir
665 663 notebook_id = nbm.copy_notebook(notebook_id)
666 self.render(
667 'notebook.html', project=project,
668 notebook_id=notebook_id,
669 base_project_url=self.application.ipython_app.base_project_url,
670 base_kernel_url=self.application.ipython_app.base_kernel_url,
671 kill_kernel=False,
672 read_only=False,
673 logged_in=self.logged_in,
674 login_available=self.login_available,
675 mathjax_url=self.application.ipython_app.mathjax_url,
676 )
664 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
677 665
678 666
679 667 #-----------------------------------------------------------------------------
680 668 # Cluster handlers
681 669 #-----------------------------------------------------------------------------
682 670
683 671
684 672 class MainClusterHandler(AuthenticatedHandler):
685 673
686 674 @web.authenticated
687 675 def get(self):
688 676 cm = self.application.cluster_manager
689 677 self.finish(jsonapi.dumps(cm.list_profiles()))
690 678
691 679
692 680 class ClusterProfileHandler(AuthenticatedHandler):
693 681
694 682 @web.authenticated
695 683 def get(self, profile):
696 684 cm = self.application.cluster_manager
697 685 self.finish(jsonapi.dumps(cm.profile_info(profile)))
698 686
699 687
700 688 class ClusterActionHandler(AuthenticatedHandler):
701 689
702 690 @web.authenticated
703 691 def post(self, profile, action):
704 692 cm = self.application.cluster_manager
705 693 if action == 'start':
706 694 n = self.get_argument('n',default=None)
707 695 if n is None:
708 696 data = cm.start_cluster(profile)
709 697 else:
710 698 data = cm.start_cluster(profile,int(n))
711 699 if action == 'stop':
712 700 data = cm.stop_cluster(profile)
713 701 self.finish(jsonapi.dumps(data))
714 702
715 703
716 704 #-----------------------------------------------------------------------------
717 705 # RST web service handlers
718 706 #-----------------------------------------------------------------------------
719 707
720 708
721 709 class RSTHandler(AuthenticatedHandler):
722 710
723 711 @web.authenticated
724 712 def post(self):
725 713 if publish_string is None:
726 714 raise web.HTTPError(503, u'docutils not available')
727 715 body = self.request.body.strip()
728 716 source = body
729 717 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
730 718 defaults = {'file_insertion_enabled': 0,
731 719 'raw_enabled': 0,
732 720 '_disable_config': 1,
733 721 'stylesheet_path': 0
734 722 # 'template': template_path
735 723 }
736 724 try:
737 725 html = publish_string(source, writer_name='html',
738 726 settings_overrides=defaults
739 727 )
740 728 except:
741 729 raise web.HTTPError(400, u'Invalid RST')
742 730 print html
743 731 self.set_header('Content-Type', 'text/html')
744 732 self.finish(html)
745 733
746 734 # to minimize subclass changes:
747 735 HTTPError = web.HTTPError
748 736
749 737 class FileFindHandler(web.StaticFileHandler):
750 738 """subclass of StaticFileHandler for serving files from a search path"""
751 739
752 740 _static_paths = {}
753 741 # _lock is needed for tornado < 2.2.0 compat
754 742 _lock = threading.Lock() # protects _static_hashes
755 743
756 744 def initialize(self, path, default_filename=None):
757 745 if isinstance(path, basestring):
758 746 path = [path]
759 747 self.roots = tuple(
760 748 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
761 749 )
762 750 self.default_filename = default_filename
763 751
764 752 @classmethod
765 753 def locate_file(cls, path, roots):
766 754 """locate a file to serve on our static file search path"""
767 755 with cls._lock:
768 756 if path in cls._static_paths:
769 757 return cls._static_paths[path]
770 758 try:
771 759 abspath = os.path.abspath(filefind(path, roots))
772 760 except IOError:
773 761 # empty string should always give exists=False
774 762 return ''
775 763
776 764 # os.path.abspath strips a trailing /
777 765 # it needs to be temporarily added back for requests to root/
778 766 if not (abspath + os.path.sep).startswith(roots):
779 767 raise HTTPError(403, "%s is not in root static directory", path)
780 768
781 769 cls._static_paths[path] = abspath
782 770 return abspath
783 771
784 772 def get(self, path, include_body=True):
785 773 path = self.parse_url_path(path)
786 774
787 775 # begin subclass override
788 776 abspath = self.locate_file(path, self.roots)
789 777 # end subclass override
790 778
791 779 if os.path.isdir(abspath) and self.default_filename is not None:
792 780 # need to look at the request.path here for when path is empty
793 781 # but there is some prefix to the path that was already
794 782 # trimmed by the routing
795 783 if not self.request.path.endswith("/"):
796 784 self.redirect(self.request.path + "/")
797 785 return
798 786 abspath = os.path.join(abspath, self.default_filename)
799 787 if not os.path.exists(abspath):
800 788 raise HTTPError(404)
801 789 if not os.path.isfile(abspath):
802 790 raise HTTPError(403, "%s is not a file", path)
803 791
804 792 stat_result = os.stat(abspath)
805 793 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
806 794
807 795 self.set_header("Last-Modified", modified)
808 796
809 797 mime_type, encoding = mimetypes.guess_type(abspath)
810 798 if mime_type:
811 799 self.set_header("Content-Type", mime_type)
812 800
813 801 cache_time = self.get_cache_time(path, modified, mime_type)
814 802
815 803 if cache_time > 0:
816 804 self.set_header("Expires", datetime.datetime.utcnow() + \
817 805 datetime.timedelta(seconds=cache_time))
818 806 self.set_header("Cache-Control", "max-age=" + str(cache_time))
819 807 else:
820 808 self.set_header("Cache-Control", "public")
821 809
822 810 self.set_extra_headers(path)
823 811
824 812 # Check the If-Modified-Since, and don't send the result if the
825 813 # content has not been modified
826 814 ims_value = self.request.headers.get("If-Modified-Since")
827 815 if ims_value is not None:
828 816 date_tuple = email.utils.parsedate(ims_value)
829 817 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
830 818 if if_since >= modified:
831 819 self.set_status(304)
832 820 return
833 821
834 822 with open(abspath, "rb") as file:
835 823 data = file.read()
836 824 hasher = hashlib.sha1()
837 825 hasher.update(data)
838 826 self.set_header("Etag", '"%s"' % hasher.hexdigest())
839 827 if include_body:
840 828 self.write(data)
841 829 else:
842 830 assert self.request.method == "HEAD"
843 831 self.set_header("Content-Length", len(data))
844 832
845 833 @classmethod
846 834 def get_version(cls, settings, path):
847 835 """Generate the version string to be used in static URLs.
848 836
849 837 This method may be overridden in subclasses (but note that it
850 838 is a class method rather than a static method). The default
851 839 implementation uses a hash of the file's contents.
852 840
853 841 ``settings`` is the `Application.settings` dictionary and ``path``
854 842 is the relative location of the requested asset on the filesystem.
855 843 The returned value should be a string, or ``None`` if no version
856 844 could be determined.
857 845 """
858 846 # begin subclass override:
859 847 static_paths = settings['static_path']
860 848 if isinstance(static_paths, basestring):
861 849 static_paths = [static_paths]
862 850 roots = tuple(
863 851 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
864 852 )
865 853
866 854 try:
867 855 abs_path = filefind(path, roots)
868 856 except IOError:
869 857 logging.error("Could not find static file %r", path)
870 858 return None
871 859
872 860 # end subclass override
873 861
874 862 with cls._lock:
875 863 hashes = cls._static_hashes
876 864 if abs_path not in hashes:
877 865 try:
878 866 f = open(abs_path, "rb")
879 867 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
880 868 f.close()
881 869 except Exception:
882 870 logging.error("Could not open static file %r", path)
883 871 hashes[abs_path] = None
884 872 hsh = hashes.get(abs_path)
885 873 if hsh:
886 874 return hsh[:5]
887 875 return None
888 876
889 877
890 878 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
891 879 # but needed for tornado < 2.2.0 compat
892 880 @classmethod
893 881 def make_static_url(cls, settings, path):
894 882 """Constructs a versioned url for the given path.
895 883
896 884 This method may be overridden in subclasses (but note that it is
897 885 a class method rather than an instance method).
898 886
899 887 ``settings`` is the `Application.settings` dictionary. ``path``
900 888 is the static path being requested. The url returned should be
901 889 relative to the current host.
902 890 """
903 891 static_url_prefix = settings.get('static_url_prefix', '/static/')
904 892 version_hash = cls.get_version(settings, path)
905 893 if version_hash:
906 894 return static_url_prefix + path + "?v=" + version_hash
907 895 return static_url_prefix + path
908 896
909 897 def parse_url_path(self, url_path):
910 898 """Converts a static URL path into a filesystem path.
911 899
912 900 ``url_path`` is the path component of the URL with
913 901 ``static_url_prefix`` removed. The return value should be
914 902 filesystem path relative to ``static_path``.
915 903 """
916 904 if os.path.sep != "/":
917 905 url_path = url_path.replace("/", os.path.sep)
918 906 return url_path
919 907
920 908
@@ -1,68 +1,67 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 // On document ready
10 10 //============================================================================
11 11
12 12
13 13 $(document).ready(function () {
14 14
15 15 IPython.init_mathjax();
16 16
17 17 IPython.read_only = $('body').data('readOnly') === 'True';
18 18 $('div#main_app').addClass('border-box-sizing ui-widget');
19 19 $('div#notebook_panel').addClass('border-box-sizing ui-widget');
20 20 // The header's bottom border is provided by the menu bar so we remove it.
21 21 $('div#header').css('border-bottom-style','none');
22 22
23 23 IPython.page = new IPython.Page();
24 24 IPython.markdown_converter = new Markdown.Converter();
25 25 IPython.layout_manager = new IPython.LayoutManager();
26 26 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
27 27 IPython.quick_help = new IPython.QuickHelp('span#quick_help_area');
28 28 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
29 29 IPython.notebook = new IPython.Notebook('div#notebook');
30 30 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
31 31 IPython.menubar = new IPython.MenuBar('#menubar')
32 32 IPython.toolbar = new IPython.ToolBar('#toolbar')
33 33 IPython.tooltip = new IPython.Tooltip()
34 34 IPython.notification_widget = new IPython.NotificationWidget('#notification')
35 35
36 36 IPython.layout_manager.do_resize();
37 37
38 38 $('body').append('<div id="fonttest"><pre><span id="test1">x</span>'+
39 39 '<span id="test2" style="font-weight: bold;">x</span>'+
40 40 '<span id="test3" style="font-style: italic;">x</span></pre></div>')
41 41 var nh = $('#test1').innerHeight();
42 42 var bh = $('#test2').innerHeight();
43 43 var ih = $('#test3').innerHeight();
44 44 if(nh != bh || nh != ih) {
45 45 $('head').append('<style>.CodeMirror span { vertical-align: bottom; }</style>');
46 46 }
47 47 $('#fonttest').remove();
48 48
49 49 if(IPython.read_only){
50 50 // hide various elements from read-only view
51 51 $('div#pager').remove();
52 52 $('div#pager_splitter').remove();
53 53
54 54 // set the notebook name field as not modifiable
55 55 $('#notebook_name').attr('disabled','disabled')
56 56 }
57 57
58 58 IPython.page.show();
59 59
60 60 IPython.layout_manager.do_resize();
61 61 $([IPython.events]).on('notebook_loaded.Notebook', function () {
62 62 IPython.layout_manager.do_resize();
63 IPython.save_widget.update_url();
64 63 })
65 64 IPython.notebook.load_notebook($('body').data('notebookId'));
66 65
67 66 });
68 67
@@ -1,148 +1,139 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 // SaveWidget
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var utils = IPython.utils;
15 15
16 16 var SaveWidget = function (selector) {
17 17 this.selector = selector;
18 18 if (this.selector !== undefined) {
19 19 this.element = $(selector);
20 20 this.style();
21 21 this.bind_events();
22 22 }
23 23 };
24 24
25 25
26 26 SaveWidget.prototype.style = function () {
27 27 this.element.find('span#save_widget').addClass('ui-widget');
28 28 this.element.find('span#notebook_name').addClass('ui-widget ui-widget-content');
29 29 this.element.find('span#save_status').addClass('ui-widget ui-widget-content')
30 30 .css({border: 'none', 'margin-left': '20px'});
31 31 };
32 32
33 33
34 34 SaveWidget.prototype.bind_events = function () {
35 35 var that = this;
36 36 this.element.find('span#notebook_name').click(function () {
37 37 that.rename_notebook();
38 38 });
39 39 this.element.find('span#notebook_name').hover(function () {
40 40 $(this).addClass("ui-state-hover");
41 41 }, function () {
42 42 $(this).removeClass("ui-state-hover");
43 43 });
44 44 $([IPython.events]).on('notebook_loaded.Notebook', function () {
45 45 that.set_last_saved();
46 46 that.update_notebook_name();
47 47 that.update_document_title();
48 48 });
49 49 $([IPython.events]).on('notebook_saved.Notebook', function () {
50 50 that.set_last_saved();
51 51 that.update_notebook_name();
52 52 that.update_document_title();
53 53 });
54 54 $([IPython.events]).on('notebook_save_failed.Notebook', function () {
55 55 that.set_save_status('Last Save Failed!');
56 56 });
57 57 };
58 58
59 59
60 60 SaveWidget.prototype.rename_notebook = function () {
61 61 var that = this;
62 62 var dialog = $('<div/>');
63 63 dialog.append(
64 64 $('<h3/>').html('Enter a new notebook name:')
65 65 .css({'margin-bottom': '10px'})
66 66 );
67 67 dialog.append(
68 68 $('<input/>').attr('type','text').attr('size','25')
69 69 .addClass('ui-widget ui-widget-content')
70 70 .attr('value',IPython.notebook.get_notebook_name())
71 71 );
72 72 // $(document).append(dialog);
73 73 dialog.dialog({
74 74 resizable: false,
75 75 modal: true,
76 76 title: "Rename Notebook",
77 77 closeText: "",
78 78 close: function(event, ui) {$(this).dialog('destroy').remove();},
79 79 buttons : {
80 80 "OK": function () {
81 81 var new_name = $(this).find('input').attr('value');
82 82 if (!IPython.notebook.test_notebook_name(new_name)) {
83 83 $(this).find('h3').html(
84 84 "Invalid notebook name. Notebook names must "+
85 85 "have 1 or more characters and can contain any characters " +
86 86 "except :/\\. Please enter a new notebook name:"
87 87 );
88 88 } else {
89 89 IPython.notebook.set_notebook_name(new_name);
90 90 IPython.notebook.save_notebook();
91 91 $(this).dialog('close');
92 92 }
93 93 },
94 94 "Cancel": function () {
95 95 $(this).dialog('close');
96 96 }
97 97 },
98 98 open : function (event, ui) {
99 99 var that = $(this);
100 100 // Upon ENTER, click the OK button.
101 101 that.find('input[type="text"]').keydown(function (event, ui) {
102 102 if (event.which === utils.keycodes.ENTER) {
103 103 that.parent().find('button').first().click();
104 104 }
105 105 });
106 106 }
107 107 });
108 108 }
109 109
110 110
111 111 SaveWidget.prototype.update_notebook_name = function () {
112 112 var nbname = IPython.notebook.get_notebook_name();
113 113 this.element.find('span#notebook_name').html(nbname);
114 114 };
115 115
116 116
117 117 SaveWidget.prototype.update_document_title = function () {
118 118 var nbname = IPython.notebook.get_notebook_name();
119 119 document.title = nbname;
120 120 };
121 121
122 122
123 SaveWidget.prototype.update_url = function () {
124 var notebook_id = IPython.notebook.get_notebook_id();
125 if (notebook_id !== null) {
126 var new_url = $('body').data('baseProjectUrl') + notebook_id;
127 window.history.replaceState({}, '', new_url);
128 };
129 };
130
131
132 123 SaveWidget.prototype.set_save_status = function (msg) {
133 124 this.element.find('span#save_status').html(msg);
134 125 }
135 126
136 127
137 128 SaveWidget.prototype.set_last_saved = function () {
138 129 var d = new Date();
139 130 this.set_save_status('Last saved: '+d.format('mmm dd h:MM TT'));
140 131 };
141 132
142 133
143 134 IPython.SaveWidget = SaveWidget;
144 135
145 136 return IPython;
146 137
147 138 }(IPython));
148 139
General Comments 0
You need to be logged in to leave comments. Login now