##// END OF EJS Templates
Merge pull request #2175 from minrk/staticfile...
Bussonnier Matthias -
r8044:0eec72e0 merge
parent child Browse files
Show More
@@ -0,0 +1,7
1 /*
2 Placeholder for custom user CSS
3
4 mainly to be overridden in profile/static/css/custom.css
5
6 This will always be an empty file in IPython
7 */ No newline at end of file
@@ -0,0 +1,7
1 /*
2 Placeholder for custom user javascript
3
4 mainly to be overridden in profile/static/js/custom.js
5
6 This will always be an empty file in IPython
7 */
@@ -1,82 +1,93
1 1 """Utility function for installing MathJax javascript library into
2 2 the notebook's 'static' directory, for offline use.
3 3
4 4 Authors:
5 5
6 6 * Min RK
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2008-2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import os
21 21 import shutil
22 22 import urllib2
23 23 import tempfile
24 24 import tarfile
25 25
26 from IPython.frontend.html import notebook as nbmod
26 from IPython.utils.path import locate_profile
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Imports
30 30 #-----------------------------------------------------------------------------
31 31
32 def install_mathjax(tag='v1.1', replace=False):
32 def install_mathjax(tag='v2.0', replace=False, dest=None):
33 33 """Download and install MathJax for offline use.
34 34
35 This will install mathjax to the 'static' dir in the IPython notebook
36 package, so it will fail if the caller does not have write access
37 to that location.
35 You can use this to install mathjax to a location on your static file
36 path. This includes the `static` directory within your IPython profile,
37 which is the default location for this install.
38 38
39 39 MathJax is a ~15MB download, and ~150MB installed.
40 40
41 41 Parameters
42 42 ----------
43 43
44 44 replace : bool [False]
45 45 Whether to remove and replace an existing install.
46 tag : str ['v1.1']
47 Which tag to download. Default is 'v1.1', the current stable release,
48 but alternatives include 'v1.1a' and 'master'.
46 tag : str ['v2.0']
47 Which tag to download. Default is 'v2.0', the current stable release,
48 but alternatives include 'v1.1' and 'master'.
49 dest : path
50 The path to the directory in which mathjax will be installed.
51 The default is `IPYTHONDIR/profile_default/static`.
52 dest must be on your notebook static_path when you run the notebook server.
53 The default location works for this.
49 54 """
50 mathjax_url = "https://github.com/mathjax/MathJax/tarball/%s"%tag
51 55
52 nbdir = os.path.dirname(os.path.abspath(nbmod.__file__))
53 static = os.path.join(nbdir, 'static')
56 mathjax_url = "https://github.com/mathjax/MathJax/tarball/%s" % tag
57
58 if dest is None:
59 dest = os.path.join(locate_profile('default'), 'static')
60
61 if not os.path.exists(dest):
62 os.mkdir(dest)
63
64 static = dest
54 65 dest = os.path.join(static, 'mathjax')
55 66
56 67 # check for existence and permissions
57 68 if not os.access(static, os.W_OK):
58 raise IOError("Need have write access to %s"%static)
69 raise IOError("Need have write access to %s" % static)
59 70 if os.path.exists(dest):
60 71 if replace:
61 72 if not os.access(dest, os.W_OK):
62 raise IOError("Need have write access to %s"%dest)
73 raise IOError("Need have write access to %s" % dest)
63 74 print "removing previous MathJax install"
64 75 shutil.rmtree(dest)
65 76 else:
66 77 print "offline MathJax apparently already installed"
67 78 return
68 79
69 80 # download mathjax
70 print "Downloading mathjax source..."
81 print "Downloading mathjax source from %s ..." % mathjax_url
71 82 response = urllib2.urlopen(mathjax_url)
72 83 print "done"
73 84 # use 'r|gz' stream mode, because socket file-like objects can't seek:
74 85 tar = tarfile.open(fileobj=response.fp, mode='r|gz')
75 86 topdir = tar.firstmember.path
76 print "Extracting to %s"%dest
87 print "Extracting to %s" % dest
77 88 tar.extractall(static)
78 89 # it will be mathjax-MathJax-<sha>, rename to just mathjax
79 90 os.rename(os.path.join(static, topdir), dest)
80 91
81 92
82 93 __all__ = ['install_mathjax']
@@ -1,738 +1,920
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 import logging
20 19 import Cookie
20 import datetime
21 import email.utils
22 import hashlib
23 import logging
24 import mimetypes
25 import os
26 import stat
27 import threading
21 28 import time
22 29 import uuid
23 30
24 31 from tornado import web
25 32 from tornado import websocket
26 33
27 34 from zmq.eventloop import ioloop
28 35 from zmq.utils import jsonapi
29 36
30 37 from IPython.external.decorator import decorator
31 38 from IPython.zmq.session import Session
32 39 from IPython.lib.security import passwd_check
33 40 from IPython.utils.jsonutil import date_default
41 from IPython.utils.path import filefind
34 42
35 43 try:
36 44 from docutils.core import publish_string
37 45 except ImportError:
38 46 publish_string = None
39 47
40 48 #-----------------------------------------------------------------------------
41 49 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
42 50 #-----------------------------------------------------------------------------
43 51
44 52 # Google Chrome, as of release 16, changed its websocket protocol number. The
45 53 # parts tornado cares about haven't really changed, so it's OK to continue
46 54 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
47 55 # version as of Oct 30/2011) the version check fails, see the issue report:
48 56
49 57 # https://github.com/facebook/tornado/issues/385
50 58
51 59 # This issue has been fixed in Tornado post 2.1.1:
52 60
53 61 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
54 62
55 63 # Here we manually apply the same patch as above so that users of IPython can
56 64 # continue to work with an officially released Tornado. We make the
57 65 # monkeypatch version check as narrow as possible to limit its effects; once
58 66 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
59 67
60 68 import tornado
61 69
62 70 if tornado.version_info <= (2,1,1):
63 71
64 72 def _execute(self, transforms, *args, **kwargs):
65 73 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
66 74
67 75 self.open_args = args
68 76 self.open_kwargs = kwargs
69 77
70 78 # The difference between version 8 and 13 is that in 8 the
71 79 # client sends a "Sec-Websocket-Origin" header and in 13 it's
72 80 # simply "Origin".
73 81 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
74 82 self.ws_connection = WebSocketProtocol8(self)
75 83 self.ws_connection.accept_connection()
76 84
77 85 elif self.request.headers.get("Sec-WebSocket-Version"):
78 86 self.stream.write(tornado.escape.utf8(
79 87 "HTTP/1.1 426 Upgrade Required\r\n"
80 88 "Sec-WebSocket-Version: 8\r\n\r\n"))
81 89 self.stream.close()
82 90
83 91 else:
84 92 self.ws_connection = WebSocketProtocol76(self)
85 93 self.ws_connection.accept_connection()
86 94
87 95 websocket.WebSocketHandler._execute = _execute
88 96 del _execute
89 97
90 98 #-----------------------------------------------------------------------------
91 99 # Decorator for disabling read-only handlers
92 100 #-----------------------------------------------------------------------------
93 101
94 102 @decorator
95 103 def not_if_readonly(f, self, *args, **kwargs):
96 104 if self.application.read_only:
97 105 raise web.HTTPError(403, "Notebook server is read-only")
98 106 else:
99 107 return f(self, *args, **kwargs)
100 108
101 109 @decorator
102 110 def authenticate_unless_readonly(f, self, *args, **kwargs):
103 111 """authenticate this page *unless* readonly view is active.
104 112
105 113 In read-only mode, the notebook list and print view should
106 114 be accessible without authentication.
107 115 """
108 116
109 117 @web.authenticated
110 118 def auth_f(self, *args, **kwargs):
111 119 return f(self, *args, **kwargs)
112 120
113 121 if self.application.read_only:
114 122 return f(self, *args, **kwargs)
115 123 else:
116 124 return auth_f(self, *args, **kwargs)
117 125
118 126 #-----------------------------------------------------------------------------
119 127 # Top-level handlers
120 128 #-----------------------------------------------------------------------------
121 129
122 130 class RequestHandler(web.RequestHandler):
123 131 """RequestHandler with default variable setting."""
124 132
125 133 def render(*args, **kwargs):
126 134 kwargs.setdefault('message', '')
127 135 return web.RequestHandler.render(*args, **kwargs)
128 136
129 137 class AuthenticatedHandler(RequestHandler):
130 138 """A RequestHandler with an authenticated user."""
131 139
132 140 def get_current_user(self):
133 141 user_id = self.get_secure_cookie("username")
134 142 # For now the user_id should not return empty, but it could eventually
135 143 if user_id == '':
136 144 user_id = 'anonymous'
137 145 if user_id is None:
138 146 # prevent extra Invalid cookie sig warnings:
139 147 self.clear_cookie('username')
140 148 if not self.application.password and not self.application.read_only:
141 149 user_id = 'anonymous'
142 150 return user_id
143 151
144 152 @property
145 153 def logged_in(self):
146 154 """Is a user currently logged in?
147 155
148 156 """
149 157 user = self.get_current_user()
150 158 return (user and not user == 'anonymous')
151 159
152 160 @property
153 161 def login_available(self):
154 162 """May a user proceed to log in?
155 163
156 164 This returns True if login capability is available, irrespective of
157 165 whether the user is already logged in or not.
158 166
159 167 """
160 168 return bool(self.application.password)
161 169
162 170 @property
163 171 def read_only(self):
164 172 """Is the notebook read-only?
165 173
166 174 """
167 175 return self.application.read_only
168 176
169 177 @property
170 178 def ws_url(self):
171 179 """websocket url matching the current request
172 180
173 181 turns http[s]://host[:port] into
174 182 ws[s]://host[:port]
175 183 """
176 184 proto = self.request.protocol.replace('http', 'ws')
177 185 host = self.application.ipython_app.websocket_host # default to config value
178 186 if host == '':
179 187 host = self.request.host # get from request
180 188 return "%s://%s" % (proto, host)
181 189
182 190
183 191 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
184 192 """static files should only be accessible when logged in"""
185 193
186 194 @authenticate_unless_readonly
187 195 def get(self, path):
188 196 return web.StaticFileHandler.get(self, path)
189 197
190 198
191 199 class ProjectDashboardHandler(AuthenticatedHandler):
192 200
193 201 @authenticate_unless_readonly
194 202 def get(self):
195 203 nbm = self.application.notebook_manager
196 204 project = nbm.notebook_dir
197 205 self.render(
198 206 'projectdashboard.html', project=project,
199 207 base_project_url=self.application.ipython_app.base_project_url,
200 208 base_kernel_url=self.application.ipython_app.base_kernel_url,
201 209 read_only=self.read_only,
202 210 logged_in=self.logged_in,
203 211 login_available=self.login_available
204 212 )
205 213
206 214
207 215 class LoginHandler(AuthenticatedHandler):
208 216
209 217 def _render(self, message=None):
210 218 self.render('login.html',
211 219 next=self.get_argument('next', default=self.application.ipython_app.base_project_url),
212 220 read_only=self.read_only,
213 221 logged_in=self.logged_in,
214 222 login_available=self.login_available,
215 223 base_project_url=self.application.ipython_app.base_project_url,
216 224 message=message
217 225 )
218 226
219 227 def get(self):
220 228 if self.current_user:
221 229 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
222 230 else:
223 231 self._render()
224 232
225 233 def post(self):
226 234 pwd = self.get_argument('password', default=u'')
227 235 if self.application.password:
228 236 if passwd_check(self.application.password, pwd):
229 237 self.set_secure_cookie('username', str(uuid.uuid4()))
230 238 else:
231 239 self._render(message={'error': 'Invalid password'})
232 240 return
233 241
234 242 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
235 243
236 244
237 245 class LogoutHandler(AuthenticatedHandler):
238 246
239 247 def get(self):
240 248 self.clear_cookie('username')
241 249 if self.login_available:
242 250 message = {'info': 'Successfully logged out.'}
243 251 else:
244 252 message = {'warning': 'Cannot log out. Notebook authentication '
245 253 'is disabled.'}
246 254
247 255 self.render('logout.html',
248 256 read_only=self.read_only,
249 257 logged_in=self.logged_in,
250 258 login_available=self.login_available,
251 259 base_project_url=self.application.ipython_app.base_project_url,
252 260 message=message)
253 261
254 262
255 263 class NewHandler(AuthenticatedHandler):
256 264
257 265 @web.authenticated
258 266 def get(self):
259 267 nbm = self.application.notebook_manager
260 268 project = nbm.notebook_dir
261 269 notebook_id = nbm.new_notebook()
262 270 self.render(
263 271 'notebook.html', project=project,
264 272 notebook_id=notebook_id,
265 273 base_project_url=self.application.ipython_app.base_project_url,
266 274 base_kernel_url=self.application.ipython_app.base_kernel_url,
267 275 kill_kernel=False,
268 276 read_only=False,
269 277 logged_in=self.logged_in,
270 278 login_available=self.login_available,
271 279 mathjax_url=self.application.ipython_app.mathjax_url,
272 280 )
273 281
274 282
275 283 class NamedNotebookHandler(AuthenticatedHandler):
276 284
277 285 @authenticate_unless_readonly
278 286 def get(self, notebook_id):
279 287 nbm = self.application.notebook_manager
280 288 project = nbm.notebook_dir
281 289 if not nbm.notebook_exists(notebook_id):
282 290 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
283 291
284 292 self.render(
285 293 'notebook.html', project=project,
286 294 notebook_id=notebook_id,
287 295 base_project_url=self.application.ipython_app.base_project_url,
288 296 base_kernel_url=self.application.ipython_app.base_kernel_url,
289 297 kill_kernel=False,
290 298 read_only=self.read_only,
291 299 logged_in=self.logged_in,
292 300 login_available=self.login_available,
293 301 mathjax_url=self.application.ipython_app.mathjax_url,
294 302 )
295 303
296 304
297 305 class PrintNotebookHandler(AuthenticatedHandler):
298 306
299 307 @authenticate_unless_readonly
300 308 def get(self, notebook_id):
301 309 nbm = self.application.notebook_manager
302 310 project = nbm.notebook_dir
303 311 if not nbm.notebook_exists(notebook_id):
304 312 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
305 313
306 314 self.render(
307 315 'printnotebook.html', project=project,
308 316 notebook_id=notebook_id,
309 317 base_project_url=self.application.ipython_app.base_project_url,
310 318 base_kernel_url=self.application.ipython_app.base_kernel_url,
311 319 kill_kernel=False,
312 320 read_only=self.read_only,
313 321 logged_in=self.logged_in,
314 322 login_available=self.login_available,
315 323 mathjax_url=self.application.ipython_app.mathjax_url,
316 324 )
317 325
318 326 #-----------------------------------------------------------------------------
319 327 # Kernel handlers
320 328 #-----------------------------------------------------------------------------
321 329
322 330
323 331 class MainKernelHandler(AuthenticatedHandler):
324 332
325 333 @web.authenticated
326 334 def get(self):
327 335 km = self.application.kernel_manager
328 336 self.finish(jsonapi.dumps(km.kernel_ids))
329 337
330 338 @web.authenticated
331 339 def post(self):
332 340 km = self.application.kernel_manager
333 341 nbm = self.application.notebook_manager
334 342 notebook_id = self.get_argument('notebook', default=None)
335 343 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
336 344 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
337 345 self.set_header('Location', '/'+kernel_id)
338 346 self.finish(jsonapi.dumps(data))
339 347
340 348
341 349 class KernelHandler(AuthenticatedHandler):
342 350
343 351 SUPPORTED_METHODS = ('DELETE')
344 352
345 353 @web.authenticated
346 354 def delete(self, kernel_id):
347 355 km = self.application.kernel_manager
348 356 km.shutdown_kernel(kernel_id)
349 357 self.set_status(204)
350 358 self.finish()
351 359
352 360
353 361 class KernelActionHandler(AuthenticatedHandler):
354 362
355 363 @web.authenticated
356 364 def post(self, kernel_id, action):
357 365 km = self.application.kernel_manager
358 366 if action == 'interrupt':
359 367 km.interrupt_kernel(kernel_id)
360 368 self.set_status(204)
361 369 if action == 'restart':
362 370 new_kernel_id = km.restart_kernel(kernel_id)
363 371 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
364 372 self.set_header('Location', '/'+new_kernel_id)
365 373 self.write(jsonapi.dumps(data))
366 374 self.finish()
367 375
368 376
369 377 class ZMQStreamHandler(websocket.WebSocketHandler):
370 378
371 379 def _reserialize_reply(self, msg_list):
372 380 """Reserialize a reply message using JSON.
373 381
374 382 This takes the msg list from the ZMQ socket, unserializes it using
375 383 self.session and then serializes the result using JSON. This method
376 384 should be used by self._on_zmq_reply to build messages that can
377 385 be sent back to the browser.
378 386 """
379 387 idents, msg_list = self.session.feed_identities(msg_list)
380 388 msg = self.session.unserialize(msg_list)
381 389 try:
382 390 msg['header'].pop('date')
383 391 except KeyError:
384 392 pass
385 393 try:
386 394 msg['parent_header'].pop('date')
387 395 except KeyError:
388 396 pass
389 397 msg.pop('buffers')
390 398 return jsonapi.dumps(msg, default=date_default)
391 399
392 400 def _on_zmq_reply(self, msg_list):
393 401 try:
394 402 msg = self._reserialize_reply(msg_list)
395 403 except Exception:
396 404 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
397 405 else:
398 406 self.write_message(msg)
399 407
400 408 def allow_draft76(self):
401 409 """Allow draft 76, until browsers such as Safari update to RFC 6455.
402 410
403 411 This has been disabled by default in tornado in release 2.2.0, and
404 412 support will be removed in later versions.
405 413 """
406 414 return True
407 415
408 416
409 417 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
410 418
411 419 def open(self, kernel_id):
412 420 self.kernel_id = kernel_id.decode('ascii')
413 421 try:
414 422 cfg = self.application.ipython_app.config
415 423 except AttributeError:
416 424 # protect from the case where this is run from something other than
417 425 # the notebook app:
418 426 cfg = None
419 427 self.session = Session(config=cfg)
420 428 self.save_on_message = self.on_message
421 429 self.on_message = self.on_first_message
422 430
423 431 def get_current_user(self):
424 432 user_id = self.get_secure_cookie("username")
425 433 if user_id == '' or (user_id is None and not self.application.password):
426 434 user_id = 'anonymous'
427 435 return user_id
428 436
429 437 def _inject_cookie_message(self, msg):
430 438 """Inject the first message, which is the document cookie,
431 439 for authentication."""
432 440 if isinstance(msg, unicode):
433 441 # Cookie can't constructor doesn't accept unicode strings for some reason
434 442 msg = msg.encode('utf8', 'replace')
435 443 try:
436 444 self.request._cookies = Cookie.SimpleCookie(msg)
437 445 except:
438 446 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
439 447
440 448 def on_first_message(self, msg):
441 449 self._inject_cookie_message(msg)
442 450 if self.get_current_user() is None:
443 451 logging.warn("Couldn't authenticate WebSocket connection")
444 452 raise web.HTTPError(403)
445 453 self.on_message = self.save_on_message
446 454
447 455
448 456 class IOPubHandler(AuthenticatedZMQStreamHandler):
449 457
450 458 def initialize(self, *args, **kwargs):
451 459 self._kernel_alive = True
452 460 self._beating = False
453 461 self.iopub_stream = None
454 462 self.hb_stream = None
455 463
456 464 def on_first_message(self, msg):
457 465 try:
458 466 super(IOPubHandler, self).on_first_message(msg)
459 467 except web.HTTPError:
460 468 self.close()
461 469 return
462 470 km = self.application.kernel_manager
463 471 self.time_to_dead = km.time_to_dead
464 472 self.first_beat = km.first_beat
465 473 kernel_id = self.kernel_id
466 474 try:
467 475 self.iopub_stream = km.create_iopub_stream(kernel_id)
468 476 self.hb_stream = km.create_hb_stream(kernel_id)
469 477 except web.HTTPError:
470 478 # WebSockets don't response to traditional error codes so we
471 479 # close the connection.
472 480 if not self.stream.closed():
473 481 self.stream.close()
474 482 self.close()
475 483 else:
476 484 self.iopub_stream.on_recv(self._on_zmq_reply)
477 485 self.start_hb(self.kernel_died)
478 486
479 487 def on_message(self, msg):
480 488 pass
481 489
482 490 def on_close(self):
483 491 # This method can be called twice, once by self.kernel_died and once
484 492 # from the WebSocket close event. If the WebSocket connection is
485 493 # closed before the ZMQ streams are setup, they could be None.
486 494 self.stop_hb()
487 495 if self.iopub_stream is not None and not self.iopub_stream.closed():
488 496 self.iopub_stream.on_recv(None)
489 497 self.iopub_stream.close()
490 498 if self.hb_stream is not None and not self.hb_stream.closed():
491 499 self.hb_stream.close()
492 500
493 501 def start_hb(self, callback):
494 502 """Start the heartbeating and call the callback if the kernel dies."""
495 503 if not self._beating:
496 504 self._kernel_alive = True
497 505
498 506 def ping_or_dead():
499 507 self.hb_stream.flush()
500 508 if self._kernel_alive:
501 509 self._kernel_alive = False
502 510 self.hb_stream.send(b'ping')
503 511 # flush stream to force immediate socket send
504 512 self.hb_stream.flush()
505 513 else:
506 514 try:
507 515 callback()
508 516 except:
509 517 pass
510 518 finally:
511 519 self.stop_hb()
512 520
513 521 def beat_received(msg):
514 522 self._kernel_alive = True
515 523
516 524 self.hb_stream.on_recv(beat_received)
517 525 loop = ioloop.IOLoop.instance()
518 526 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
519 527 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
520 528 self._beating= True
521 529
522 530 def _really_start_hb(self):
523 531 """callback for delayed heartbeat start
524 532
525 533 Only start the hb loop if we haven't been closed during the wait.
526 534 """
527 535 if self._beating and not self.hb_stream.closed():
528 536 self._hb_periodic_callback.start()
529 537
530 538 def stop_hb(self):
531 539 """Stop the heartbeating and cancel all related callbacks."""
532 540 if self._beating:
533 541 self._beating = False
534 542 self._hb_periodic_callback.stop()
535 543 if not self.hb_stream.closed():
536 544 self.hb_stream.on_recv(None)
537 545
538 546 def kernel_died(self):
539 547 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
540 548 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
541 549 self.write_message(
542 550 {'header': {'msg_type': 'status'},
543 551 'parent_header': {},
544 552 'content': {'execution_state':'dead'}
545 553 }
546 554 )
547 555 self.on_close()
548 556
549 557
550 558 class ShellHandler(AuthenticatedZMQStreamHandler):
551 559
552 560 def initialize(self, *args, **kwargs):
553 561 self.shell_stream = None
554 562
555 563 def on_first_message(self, msg):
556 564 try:
557 565 super(ShellHandler, self).on_first_message(msg)
558 566 except web.HTTPError:
559 567 self.close()
560 568 return
561 569 km = self.application.kernel_manager
562 570 self.max_msg_size = km.max_msg_size
563 571 kernel_id = self.kernel_id
564 572 try:
565 573 self.shell_stream = km.create_shell_stream(kernel_id)
566 574 except web.HTTPError:
567 575 # WebSockets don't response to traditional error codes so we
568 576 # close the connection.
569 577 if not self.stream.closed():
570 578 self.stream.close()
571 579 self.close()
572 580 else:
573 581 self.shell_stream.on_recv(self._on_zmq_reply)
574 582
575 583 def on_message(self, msg):
576 584 if len(msg) < self.max_msg_size:
577 585 msg = jsonapi.loads(msg)
578 586 self.session.send(self.shell_stream, msg)
579 587
580 588 def on_close(self):
581 589 # Make sure the stream exists and is not already closed.
582 590 if self.shell_stream is not None and not self.shell_stream.closed():
583 591 self.shell_stream.close()
584 592
585 593
586 594 #-----------------------------------------------------------------------------
587 595 # Notebook web service handlers
588 596 #-----------------------------------------------------------------------------
589 597
590 598 class NotebookRootHandler(AuthenticatedHandler):
591 599
592 600 @authenticate_unless_readonly
593 601 def get(self):
594 602 nbm = self.application.notebook_manager
595 603 km = self.application.kernel_manager
596 604 files = nbm.list_notebooks()
597 605 for f in files :
598 606 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
599 607 self.finish(jsonapi.dumps(files))
600 608
601 609 @web.authenticated
602 610 def post(self):
603 611 nbm = self.application.notebook_manager
604 612 body = self.request.body.strip()
605 613 format = self.get_argument('format', default='json')
606 614 name = self.get_argument('name', default=None)
607 615 if body:
608 616 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
609 617 else:
610 618 notebook_id = nbm.new_notebook()
611 619 self.set_header('Location', '/'+notebook_id)
612 620 self.finish(jsonapi.dumps(notebook_id))
613 621
614 622
615 623 class NotebookHandler(AuthenticatedHandler):
616 624
617 625 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
618 626
619 627 @authenticate_unless_readonly
620 628 def get(self, notebook_id):
621 629 nbm = self.application.notebook_manager
622 630 format = self.get_argument('format', default='json')
623 631 last_mod, name, data = nbm.get_notebook(notebook_id, format)
624 632
625 633 if format == u'json':
626 634 self.set_header('Content-Type', 'application/json')
627 635 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
628 636 elif format == u'py':
629 637 self.set_header('Content-Type', 'application/x-python')
630 638 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
631 639 self.set_header('Last-Modified', last_mod)
632 640 self.finish(data)
633 641
634 642 @web.authenticated
635 643 def put(self, notebook_id):
636 644 nbm = self.application.notebook_manager
637 645 format = self.get_argument('format', default='json')
638 646 name = self.get_argument('name', default=None)
639 647 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
640 648 self.set_status(204)
641 649 self.finish()
642 650
643 651 @web.authenticated
644 652 def delete(self, notebook_id):
645 653 nbm = self.application.notebook_manager
646 654 nbm.delete_notebook(notebook_id)
647 655 self.set_status(204)
648 656 self.finish()
649 657
650 658
651 659 class NotebookCopyHandler(AuthenticatedHandler):
652 660
653 661 @web.authenticated
654 662 def get(self, notebook_id):
655 663 nbm = self.application.notebook_manager
656 664 project = nbm.notebook_dir
657 665 notebook_id = nbm.copy_notebook(notebook_id)
658 666 self.render(
659 667 'notebook.html', project=project,
660 668 notebook_id=notebook_id,
661 669 base_project_url=self.application.ipython_app.base_project_url,
662 670 base_kernel_url=self.application.ipython_app.base_kernel_url,
663 671 kill_kernel=False,
664 672 read_only=False,
665 673 logged_in=self.logged_in,
666 674 login_available=self.login_available,
667 675 mathjax_url=self.application.ipython_app.mathjax_url,
668 676 )
669 677
670 678
671 679 #-----------------------------------------------------------------------------
672 680 # Cluster handlers
673 681 #-----------------------------------------------------------------------------
674 682
675 683
676 684 class MainClusterHandler(AuthenticatedHandler):
677 685
678 686 @web.authenticated
679 687 def get(self):
680 688 cm = self.application.cluster_manager
681 689 self.finish(jsonapi.dumps(cm.list_profiles()))
682 690
683 691
684 692 class ClusterProfileHandler(AuthenticatedHandler):
685 693
686 694 @web.authenticated
687 695 def get(self, profile):
688 696 cm = self.application.cluster_manager
689 697 self.finish(jsonapi.dumps(cm.profile_info(profile)))
690 698
691 699
692 700 class ClusterActionHandler(AuthenticatedHandler):
693 701
694 702 @web.authenticated
695 703 def post(self, profile, action):
696 704 cm = self.application.cluster_manager
697 705 if action == 'start':
698 706 n = self.get_argument('n',default=None)
699 707 if n is None:
700 708 data = cm.start_cluster(profile)
701 709 else:
702 710 data = cm.start_cluster(profile,int(n))
703 711 if action == 'stop':
704 712 data = cm.stop_cluster(profile)
705 713 self.finish(jsonapi.dumps(data))
706 714
707 715
708 716 #-----------------------------------------------------------------------------
709 717 # RST web service handlers
710 718 #-----------------------------------------------------------------------------
711 719
712 720
713 721 class RSTHandler(AuthenticatedHandler):
714 722
715 723 @web.authenticated
716 724 def post(self):
717 725 if publish_string is None:
718 726 raise web.HTTPError(503, u'docutils not available')
719 727 body = self.request.body.strip()
720 728 source = body
721 729 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
722 730 defaults = {'file_insertion_enabled': 0,
723 731 'raw_enabled': 0,
724 732 '_disable_config': 1,
725 733 'stylesheet_path': 0
726 734 # 'template': template_path
727 735 }
728 736 try:
729 737 html = publish_string(source, writer_name='html',
730 738 settings_overrides=defaults
731 739 )
732 740 except:
733 741 raise web.HTTPError(400, u'Invalid RST')
734 742 print html
735 743 self.set_header('Content-Type', 'text/html')
736 744 self.finish(html)
737 745
746 # to minimize subclass changes:
747 HTTPError = web.HTTPError
748
749 class FileFindHandler(web.StaticFileHandler):
750 """subclass of StaticFileHandler for serving files from a search path"""
751
752 _static_paths = {}
753 # _lock is needed for tornado < 2.2.0 compat
754 _lock = threading.Lock() # protects _static_hashes
755
756 def initialize(self, path, default_filename=None):
757 if isinstance(path, basestring):
758 path = [path]
759 self.roots = tuple(
760 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
761 )
762 self.default_filename = default_filename
763
764 @classmethod
765 def locate_file(cls, path, roots):
766 """locate a file to serve on our static file search path"""
767 with cls._lock:
768 if path in cls._static_paths:
769 return cls._static_paths[path]
770 try:
771 abspath = os.path.abspath(filefind(path, roots))
772 except IOError:
773 # empty string should always give exists=False
774 return ''
775
776 # os.path.abspath strips a trailing /
777 # it needs to be temporarily added back for requests to root/
778 if not (abspath + os.path.sep).startswith(roots):
779 raise HTTPError(403, "%s is not in root static directory", path)
780
781 cls._static_paths[path] = abspath
782 return abspath
783
784 def get(self, path, include_body=True):
785 path = self.parse_url_path(path)
786
787 # begin subclass override
788 abspath = self.locate_file(path, self.roots)
789 # end subclass override
790
791 if os.path.isdir(abspath) and self.default_filename is not None:
792 # need to look at the request.path here for when path is empty
793 # but there is some prefix to the path that was already
794 # trimmed by the routing
795 if not self.request.path.endswith("/"):
796 self.redirect(self.request.path + "/")
797 return
798 abspath = os.path.join(abspath, self.default_filename)
799 if not os.path.exists(abspath):
800 raise HTTPError(404)
801 if not os.path.isfile(abspath):
802 raise HTTPError(403, "%s is not a file", path)
803
804 stat_result = os.stat(abspath)
805 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
806
807 self.set_header("Last-Modified", modified)
808
809 mime_type, encoding = mimetypes.guess_type(abspath)
810 if mime_type:
811 self.set_header("Content-Type", mime_type)
812
813 cache_time = self.get_cache_time(path, modified, mime_type)
814
815 if cache_time > 0:
816 self.set_header("Expires", datetime.datetime.utcnow() + \
817 datetime.timedelta(seconds=cache_time))
818 self.set_header("Cache-Control", "max-age=" + str(cache_time))
819 else:
820 self.set_header("Cache-Control", "public")
821
822 self.set_extra_headers(path)
823
824 # Check the If-Modified-Since, and don't send the result if the
825 # content has not been modified
826 ims_value = self.request.headers.get("If-Modified-Since")
827 if ims_value is not None:
828 date_tuple = email.utils.parsedate(ims_value)
829 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
830 if if_since >= modified:
831 self.set_status(304)
832 return
833
834 with open(abspath, "rb") as file:
835 data = file.read()
836 hasher = hashlib.sha1()
837 hasher.update(data)
838 self.set_header("Etag", '"%s"' % hasher.hexdigest())
839 if include_body:
840 self.write(data)
841 else:
842 assert self.request.method == "HEAD"
843 self.set_header("Content-Length", len(data))
844
845 @classmethod
846 def get_version(cls, settings, path):
847 """Generate the version string to be used in static URLs.
848
849 This method may be overridden in subclasses (but note that it
850 is a class method rather than a static method). The default
851 implementation uses a hash of the file's contents.
852
853 ``settings`` is the `Application.settings` dictionary and ``path``
854 is the relative location of the requested asset on the filesystem.
855 The returned value should be a string, or ``None`` if no version
856 could be determined.
857 """
858 # begin subclass override:
859 static_paths = settings['static_path']
860 if isinstance(static_paths, basestring):
861 static_paths = [static_paths]
862 roots = tuple(
863 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
864 )
865
866 try:
867 abs_path = filefind(path, roots)
868 except IOError:
869 logging.error("Could not find static file %r", path)
870 return None
871
872 # end subclass override
873
874 with cls._lock:
875 hashes = cls._static_hashes
876 if abs_path not in hashes:
877 try:
878 f = open(abs_path, "rb")
879 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
880 f.close()
881 except Exception:
882 logging.error("Could not open static file %r", path)
883 hashes[abs_path] = None
884 hsh = hashes.get(abs_path)
885 if hsh:
886 return hsh[:5]
887 return None
888
889
890 # make_static_url and parse_url_path totally unchanged from tornado 2.2.0
891 # but needed for tornado < 2.2.0 compat
892 @classmethod
893 def make_static_url(cls, settings, path):
894 """Constructs a versioned url for the given path.
895
896 This method may be overridden in subclasses (but note that it is
897 a class method rather than an instance method).
898
899 ``settings`` is the `Application.settings` dictionary. ``path``
900 is the static path being requested. The url returned should be
901 relative to the current host.
902 """
903 static_url_prefix = settings.get('static_url_prefix', '/static/')
904 version_hash = cls.get_version(settings, path)
905 if version_hash:
906 return static_url_prefix + path + "?v=" + version_hash
907 return static_url_prefix + path
908
909 def parse_url_path(self, url_path):
910 """Converts a static URL path into a filesystem path.
911
912 ``url_path`` is the path component of the URL with
913 ``static_url_prefix`` removed. The return value should be
914 filesystem path relative to ``static_path``.
915 """
916 if os.path.sep != "/":
917 url_path = url_path.replace("/", os.path.sep)
918 return url_path
919
738 920
@@ -1,593 +1,611
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
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 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import re
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
31 31 import webbrowser
32 32
33 33 # Third party
34 34 import zmq
35 35
36 36 # Install the pyzmq ioloop. This has to be done before anything else from
37 37 # tornado is imported.
38 38 from zmq.eventloop import ioloop
39 39 ioloop.install()
40 40
41 41 from tornado import httpserver
42 42 from tornado import web
43 43
44 44 # Our own libraries
45 45 from .kernelmanager import MappingKernelManager
46 46 from .handlers import (LoginHandler, LogoutHandler,
47 47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
48 48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
52 FileFindHandler,
52 53 )
53 54 from .notebookmanager import NotebookManager
54 55 from .clustermanager import ClusterManager
55 56
56 57 from IPython.config.application import catch_config_error, boolean_flag
57 58 from IPython.core.application import BaseIPythonApplication
58 59 from IPython.core.profiledir import ProfileDir
59 60 from IPython.frontend.consoleapp import IPythonConsoleApp
60 61 from IPython.lib.kernel import swallow_argv
61 62 from IPython.zmq.session import Session, default_secure
62 63 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 64 from IPython.zmq.ipkernel import (
64 65 flags as ipkernel_flags,
65 66 aliases as ipkernel_aliases,
66 67 IPKernelApp
67 68 )
68 69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 70 from IPython.utils import py3compat
71 from IPython.utils.path import filefind
70 72
71 73 #-----------------------------------------------------------------------------
72 74 # Module globals
73 75 #-----------------------------------------------------------------------------
74 76
75 77 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
76 78 _kernel_action_regex = r"(?P<action>restart|interrupt)"
77 79 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
78 80 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
79 81 _cluster_action_regex = r"(?P<action>start|stop)"
80 82
81 83
82 84 LOCALHOST = '127.0.0.1'
83 85
84 86 _examples = """
85 87 ipython notebook # start the notebook
86 88 ipython notebook --profile=sympy # use the sympy profile
87 89 ipython notebook --pylab=inline # pylab in inline plotting mode
88 90 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
89 91 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
90 92 """
91 93
92 94 #-----------------------------------------------------------------------------
93 95 # Helper functions
94 96 #-----------------------------------------------------------------------------
95 97
96 98 def url_path_join(a,b):
97 99 if a.endswith('/') and b.startswith('/'):
98 100 return a[:-1]+b
99 101 else:
100 102 return a+b
101 103
102 104 def random_ports(port, n):
103 105 """Generate a list of n random ports near the given port.
104 106
105 107 The first 5 ports will be sequential, and the remaining n-5 will be
106 108 randomly selected in the range [port-2*n, port+2*n].
107 109 """
108 110 for i in range(min(5, n)):
109 111 yield port + i
110 112 for i in range(n-5):
111 113 yield port + random.randint(-2*n, 2*n)
112 114
113 115 #-----------------------------------------------------------------------------
114 116 # The Tornado web application
115 117 #-----------------------------------------------------------------------------
116 118
117 119 class NotebookWebApplication(web.Application):
118 120
119 121 def __init__(self, ipython_app, kernel_manager, notebook_manager,
120 122 cluster_manager, log,
121 123 base_project_url, settings_overrides):
122 124 handlers = [
123 125 (r"/", ProjectDashboardHandler),
124 126 (r"/login", LoginHandler),
125 127 (r"/logout", LogoutHandler),
126 128 (r"/new", NewHandler),
127 129 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
128 130 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
129 131 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
130 132 (r"/kernels", MainKernelHandler),
131 133 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
132 134 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
133 135 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
134 136 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
135 137 (r"/notebooks", NotebookRootHandler),
136 138 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
137 139 (r"/rstservice/render", RSTHandler),
138 140 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
139 141 (r"/clusters", MainClusterHandler),
140 142 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
141 143 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
142 144 ]
143 145
144 146 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
145 147 # base_project_url will always be unicode, which will in turn
146 148 # make the patterns unicode, and ultimately result in unicode
147 149 # keys in kwargs to handler._execute(**kwargs) in tornado.
148 150 # This enforces that base_project_url be ascii in that situation.
149 151 #
150 152 # Note that the URLs these patterns check against are escaped,
151 153 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
152 154 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
153 155
154 156 settings = dict(
155 157 template_path=os.path.join(os.path.dirname(__file__), "templates"),
156 static_path=os.path.join(os.path.dirname(__file__), "static"),
158 static_path=ipython_app.static_file_path,
159 static_handler_class = FileFindHandler,
157 160 cookie_secret=os.urandom(1024),
158 161 login_url="%s/login"%(base_project_url.rstrip('/')),
159 162 )
160 163
161 164 # allow custom overrides for the tornado web app.
162 165 settings.update(settings_overrides)
163 166
164 167 # prepend base_project_url onto the patterns that we match
165 168 new_handlers = []
166 169 for handler in handlers:
167 170 pattern = url_path_join(base_project_url, handler[0])
168 171 new_handler = tuple([pattern]+list(handler[1:]))
169 172 new_handlers.append( new_handler )
170 173
171 174 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
172 175
173 176 self.kernel_manager = kernel_manager
174 177 self.notebook_manager = notebook_manager
175 178 self.cluster_manager = cluster_manager
176 179 self.ipython_app = ipython_app
177 180 self.read_only = self.ipython_app.read_only
178 181 self.log = log
179 182
180 183
181 184 #-----------------------------------------------------------------------------
182 185 # Aliases and Flags
183 186 #-----------------------------------------------------------------------------
184 187
185 188 flags = dict(ipkernel_flags)
186 189 flags['no-browser']=(
187 190 {'NotebookApp' : {'open_browser' : False}},
188 191 "Don't open the notebook in a browser after startup."
189 192 )
190 193 flags['no-mathjax']=(
191 194 {'NotebookApp' : {'enable_mathjax' : False}},
192 195 """Disable MathJax
193 196
194 197 MathJax is the javascript library IPython uses to render math/LaTeX. It is
195 198 very large, so you may want to disable it if you have a slow internet
196 199 connection, or for offline use of the notebook.
197 200
198 201 When disabled, equations etc. will appear as their untransformed TeX source.
199 202 """
200 203 )
201 204 flags['read-only'] = (
202 205 {'NotebookApp' : {'read_only' : True}},
203 206 """Allow read-only access to notebooks.
204 207
205 208 When using a password to protect the notebook server, this flag
206 209 allows unauthenticated clients to view the notebook list, and
207 210 individual notebooks, but not edit them, start kernels, or run
208 211 code.
209 212
210 213 If no password is set, the server will be entirely read-only.
211 214 """
212 215 )
213 216
214 217 # Add notebook manager flags
215 218 flags.update(boolean_flag('script', 'NotebookManager.save_script',
216 219 'Auto-save a .py script everytime the .ipynb notebook is saved',
217 220 'Do not auto-save .py scripts for every notebook'))
218 221
219 222 # the flags that are specific to the frontend
220 223 # these must be scrubbed before being passed to the kernel,
221 224 # or it will raise an error on unrecognized flags
222 225 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
223 226
224 227 aliases = dict(ipkernel_aliases)
225 228
226 229 aliases.update({
227 230 'ip': 'NotebookApp.ip',
228 231 'port': 'NotebookApp.port',
229 232 'port-retries': 'NotebookApp.port_retries',
230 233 'keyfile': 'NotebookApp.keyfile',
231 234 'certfile': 'NotebookApp.certfile',
232 235 'notebook-dir': 'NotebookManager.notebook_dir',
233 236 'browser': 'NotebookApp.browser',
234 237 })
235 238
236 239 # remove ipkernel flags that are singletons, and don't make sense in
237 240 # multi-kernel evironment:
238 241 aliases.pop('f', None)
239 242
240 243 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
241 244 u'notebook-dir']
242 245
243 246 #-----------------------------------------------------------------------------
244 247 # NotebookApp
245 248 #-----------------------------------------------------------------------------
246 249
247 250 class NotebookApp(BaseIPythonApplication):
248 251
249 252 name = 'ipython-notebook'
250 253 default_config_file_name='ipython_notebook_config.py'
251 254
252 255 description = """
253 256 The IPython HTML Notebook.
254 257
255 258 This launches a Tornado based HTML Notebook Server that serves up an
256 259 HTML5/Javascript Notebook client.
257 260 """
258 261 examples = _examples
259 262
260 263 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
261 264 flags = Dict(flags)
262 265 aliases = Dict(aliases)
263 266
264 267 kernel_argv = List(Unicode)
265 268
266 269 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
267 270 default_value=logging.INFO,
268 271 config=True,
269 272 help="Set the log level by value or name.")
270 273
271 274 # create requested profiles by default, if they don't exist:
272 275 auto_create = Bool(True)
273 276
274 277 # file to be opened in the notebook server
275 278 file_to_run = Unicode('')
276 279
277 280 # Network related information.
278 281
279 282 ip = Unicode(LOCALHOST, config=True,
280 283 help="The IP address the notebook server will listen on."
281 284 )
282 285
283 286 def _ip_changed(self, name, old, new):
284 287 if new == u'*': self.ip = u''
285 288
286 289 port = Integer(8888, config=True,
287 290 help="The port the notebook server will listen on."
288 291 )
289 292 port_retries = Integer(50, config=True,
290 293 help="The number of additional ports to try if the specified port is not available."
291 294 )
292 295
293 296 certfile = Unicode(u'', config=True,
294 297 help="""The full path to an SSL/TLS certificate file."""
295 298 )
296 299
297 300 keyfile = Unicode(u'', config=True,
298 301 help="""The full path to a private key file for usage with SSL/TLS."""
299 302 )
300 303
301 304 password = Unicode(u'', config=True,
302 305 help="""Hashed password to use for web authentication.
303 306
304 307 To generate, type in a python/IPython shell:
305 308
306 309 from IPython.lib import passwd; passwd()
307 310
308 311 The string should be of the form type:salt:hashed-password.
309 312 """
310 313 )
311 314
312 315 open_browser = Bool(True, config=True,
313 316 help="""Whether to open in a browser after starting.
314 317 The specific browser used is platform dependent and
315 318 determined by the python standard library `webbrowser`
316 319 module, unless it is overridden using the --browser
317 320 (NotebookApp.browser) configuration option.
318 321 """)
319 322
320 323 browser = Unicode(u'', config=True,
321 324 help="""Specify what command to use to invoke a web
322 325 browser when opening the notebook. If not specified, the
323 326 default browser will be determined by the `webbrowser`
324 327 standard library module, which allows setting of the
325 328 BROWSER environment variable to override it.
326 329 """)
327 330
328 331 read_only = Bool(False, config=True,
329 332 help="Whether to prevent editing/execution of notebooks."
330 333 )
331 334
332 335 webapp_settings = Dict(config=True,
333 336 help="Supply overrides for the tornado.web.Application that the "
334 337 "IPython notebook uses.")
335 338
336 339 enable_mathjax = Bool(True, config=True,
337 340 help="""Whether to enable MathJax for typesetting math/TeX
338 341
339 342 MathJax is the javascript library IPython uses to render math/LaTeX. It is
340 343 very large, so you may want to disable it if you have a slow internet
341 344 connection, or for offline use of the notebook.
342 345
343 346 When disabled, equations etc. will appear as their untransformed TeX source.
344 347 """
345 348 )
346 349 def _enable_mathjax_changed(self, name, old, new):
347 350 """set mathjax url to empty if mathjax is disabled"""
348 351 if not new:
349 352 self.mathjax_url = u''
350 353
351 354 base_project_url = Unicode('/', config=True,
352 355 help='''The base URL for the notebook server''')
353 356 base_kernel_url = Unicode('/', config=True,
354 357 help='''The base URL for the kernel server''')
355 358 websocket_host = Unicode("", config=True,
356 359 help="""The hostname for the websocket server."""
357 360 )
361
362 extra_static_paths = List(Unicode, config=True,
363 help="""Extra paths to search for serving static files.
364
365 This allows adding javascript/css to be available from the notebook server machine,
366 or overriding individual files in the IPython"""
367 )
368 def _extra_static_paths_default(self):
369 return [os.path.join(self.profile_dir.location, 'static')]
370
371 @property
372 def static_file_path(self):
373 """return extra paths + the default location"""
374 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
358 375
359 376 mathjax_url = Unicode("", config=True,
360 377 help="""The url for MathJax.js."""
361 378 )
362 379 def _mathjax_url_default(self):
363 380 if not self.enable_mathjax:
364 381 return u''
365 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
366 382 static_url_prefix = self.webapp_settings.get("static_url_prefix",
367 383 "/static/")
368 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
369 self.log.info("Using local MathJax")
370 return static_url_prefix+u"mathjax/MathJax.js"
371 else:
384 try:
385 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
386 except IOError:
372 387 if self.certfile:
373 388 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
374 389 base = u"https://c328740.ssl.cf1.rackcdn.com"
375 390 else:
376 391 base = u"http://cdn.mathjax.org"
377 392
378 393 url = base + u"/mathjax/latest/MathJax.js"
379 394 self.log.info("Using MathJax from CDN: %s", url)
380 395 return url
396 else:
397 self.log.info("Using local MathJax from %s" % mathjax)
398 return static_url_prefix+u"mathjax/MathJax.js"
381 399
382 400 def _mathjax_url_changed(self, name, old, new):
383 401 if new and not self.enable_mathjax:
384 402 # enable_mathjax=False overrides mathjax_url
385 403 self.mathjax_url = u''
386 404 else:
387 405 self.log.info("Using MathJax: %s", new)
388 406
389 407 def parse_command_line(self, argv=None):
390 408 super(NotebookApp, self).parse_command_line(argv)
391 409 if argv is None:
392 410 argv = sys.argv[1:]
393 411
394 412 # Scrub frontend-specific flags
395 413 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
396 414 # Kernel should inherit default config file from frontend
397 415 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
398 416
399 417 if self.extra_args:
400 418 f = os.path.abspath(self.extra_args[0])
401 419 if os.path.isdir(f):
402 420 nbdir = f
403 421 else:
404 422 self.file_to_run = f
405 423 nbdir = os.path.dirname(f)
406 424 self.config.NotebookManager.notebook_dir = nbdir
407 425
408 426 def init_configurables(self):
409 427 # force Session default to be secure
410 428 default_secure(self.config)
411 429 self.kernel_manager = MappingKernelManager(
412 430 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
413 431 connection_dir = self.profile_dir.security_dir,
414 432 )
415 433 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
416 434 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
417 435 self.notebook_manager.list_notebooks()
418 436 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
419 437 self.cluster_manager.update_profiles()
420 438
421 439 def init_logging(self):
422 440 # This prevents double log messages because tornado use a root logger that
423 441 # self.log is a child of. The logging module dipatches log messages to a log
424 442 # and all of its ancenstors until propagate is set to False.
425 443 self.log.propagate = False
426 444
427 445 def init_webapp(self):
428 446 """initialize tornado webapp and httpserver"""
429 447 self.web_app = NotebookWebApplication(
430 448 self, self.kernel_manager, self.notebook_manager,
431 449 self.cluster_manager, self.log,
432 450 self.base_project_url, self.webapp_settings
433 451 )
434 452 if self.certfile:
435 453 ssl_options = dict(certfile=self.certfile)
436 454 if self.keyfile:
437 455 ssl_options['keyfile'] = self.keyfile
438 456 else:
439 457 ssl_options = None
440 458 self.web_app.password = self.password
441 459 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
442 460 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
443 461 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
444 462 'but not using any encryption or authentication. This is highly '
445 463 'insecure and not recommended.')
446 464
447 465 success = None
448 466 for port in random_ports(self.port, self.port_retries+1):
449 467 try:
450 468 self.http_server.listen(port, self.ip)
451 469 except socket.error as e:
452 470 if e.errno != errno.EADDRINUSE:
453 471 raise
454 472 self.log.info('The port %i is already in use, trying another random port.' % port)
455 473 else:
456 474 self.port = port
457 475 success = True
458 476 break
459 477 if not success:
460 478 self.log.critical('ERROR: the notebook server could not be started because '
461 479 'no available port could be found.')
462 480 self.exit(1)
463 481
464 482 def init_signal(self):
465 483 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
466 484 # safely extract zmq version info:
467 485 try:
468 486 zmq_v = zmq.pyzmq_version_info()
469 487 except AttributeError:
470 488 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
471 489 if 'dev' in zmq.__version__:
472 490 zmq_v.append(999)
473 491 zmq_v = tuple(zmq_v)
474 492 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
475 493 # This won't work with 2.1.7 and
476 494 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
477 495 # but it will work
478 496 signal.signal(signal.SIGINT, self._handle_sigint)
479 497 signal.signal(signal.SIGTERM, self._signal_stop)
480 498
481 499 def _handle_sigint(self, sig, frame):
482 500 """SIGINT handler spawns confirmation dialog"""
483 501 # register more forceful signal handler for ^C^C case
484 502 signal.signal(signal.SIGINT, self._signal_stop)
485 503 # request confirmation dialog in bg thread, to avoid
486 504 # blocking the App
487 505 thread = threading.Thread(target=self._confirm_exit)
488 506 thread.daemon = True
489 507 thread.start()
490 508
491 509 def _restore_sigint_handler(self):
492 510 """callback for restoring original SIGINT handler"""
493 511 signal.signal(signal.SIGINT, self._handle_sigint)
494 512
495 513 def _confirm_exit(self):
496 514 """confirm shutdown on ^C
497 515
498 516 A second ^C, or answering 'y' within 5s will cause shutdown,
499 517 otherwise original SIGINT handler will be restored.
500 518
501 519 This doesn't work on Windows.
502 520 """
503 521 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
504 522 time.sleep(0.1)
505 523 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
506 524 sys.stdout.flush()
507 525 r,w,x = select.select([sys.stdin], [], [], 5)
508 526 if r:
509 527 line = sys.stdin.readline()
510 528 if line.lower().startswith('y'):
511 529 self.log.critical("Shutdown confirmed")
512 530 ioloop.IOLoop.instance().stop()
513 531 return
514 532 else:
515 533 print "No answer for 5s:",
516 534 print "resuming operation..."
517 535 # no answer, or answer is no:
518 536 # set it back to original SIGINT handler
519 537 # use IOLoop.add_callback because signal.signal must be called
520 538 # from main thread
521 539 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
522 540
523 541 def _signal_stop(self, sig, frame):
524 542 self.log.critical("received signal %s, stopping", sig)
525 543 ioloop.IOLoop.instance().stop()
526 544
527 545 @catch_config_error
528 546 def initialize(self, argv=None):
529 547 self.init_logging()
530 548 super(NotebookApp, self).initialize(argv)
531 549 self.init_configurables()
532 550 self.init_webapp()
533 551 self.init_signal()
534 552
535 553 def cleanup_kernels(self):
536 554 """shutdown all kernels
537 555
538 556 The kernels will shutdown themselves when this process no longer exists,
539 557 but explicit shutdown allows the KernelManagers to cleanup the connection files.
540 558 """
541 559 self.log.info('Shutting down kernels')
542 560 km = self.kernel_manager
543 561 # copy list, since shutdown_kernel deletes keys
544 562 for kid in list(km.kernel_ids):
545 563 km.shutdown_kernel(kid)
546 564
547 565 def start(self):
548 566 ip = self.ip if self.ip else '[all ip addresses on your system]'
549 567 proto = 'https' if self.certfile else 'http'
550 568 info = self.log.info
551 569 info("The IPython Notebook is running at: %s://%s:%i%s" %
552 570 (proto, ip, self.port,self.base_project_url) )
553 571 info("Use Control-C to stop this server and shut down all kernels.")
554 572
555 573 if self.open_browser or self.file_to_run:
556 574 ip = self.ip or '127.0.0.1'
557 575 try:
558 576 browser = webbrowser.get(self.browser or None)
559 577 except webbrowser.Error as e:
560 578 self.log.warn('No web browser found: %s.' % e)
561 579 browser = None
562 580
563 581 if self.file_to_run:
564 582 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
565 583 for nb in self.notebook_manager.list_notebooks():
566 584 if filename == nb['name']:
567 585 url = nb['notebook_id']
568 586 break
569 587 else:
570 588 url = ''
571 589 else:
572 590 url = ''
573 591 if browser:
574 592 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
575 593 self.port, self.base_project_url, url), new=2)
576 594 threading.Thread(target=b).start()
577 595 try:
578 596 ioloop.IOLoop.instance().start()
579 597 except KeyboardInterrupt:
580 598 info("Interrupted...")
581 599 finally:
582 600 self.cleanup_kernels()
583 601
584 602
585 603 #-----------------------------------------------------------------------------
586 604 # Main entry point
587 605 #-----------------------------------------------------------------------------
588 606
589 607 def launch_new_instance():
590 608 app = NotebookApp.instance()
591 609 app.initialize()
592 610 app.start()
593 611
@@ -1,250 +1,250
1 1 {% extends page.html %}
2 2 {% block stylesheet %}
3 3
4 4 {% if mathjax_url %}
5 5 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML" charset="utf-8"></script>
6 6 {% end %}
7 7 <script type="text/javascript">
8 8 // MathJax disabled, set as null to distingish from *missing* MathJax,
9 9 // where it will be undefined, and should prompt a dialog later.
10 10 window.mathjax_url = "{{mathjax_url}}";
11 11 </script>
12 12
13 13 <link rel="stylesheet" href="{{ static_url("codemirror/lib/codemirror.css") }}">
14 14 <link rel="stylesheet" href="{{ static_url("codemirror/theme/ipython.css") }}">
15 15
16 16 <link rel="stylesheet" href="{{ static_url("prettify/prettify.css") }}"/>
17 17
18 18 <link rel="stylesheet" href="{{ static_url("css/notebook.css") }}" type="text/css" />
19 19 <link rel="stylesheet" href="{{ static_url("css/tooltip.css") }}" type="text/css" />
20 20 <link rel="stylesheet" href="{{ static_url("css/renderedhtml.css") }}" type="text/css" />
21 21
22 22 <link rel="stylesheet" href="{{ static_url("css/printnotebook.css") }}" type="text/css" media="print"/>
23 23
24 24 {% end %}
25 25
26 26
27 27 {% block params %}
28 28
29 29 data-project={{project}}
30 30 data-base-project-url={{base_project_url}}
31 31 data-base-kernel-url={{base_kernel_url}}
32 32 data-read-only={{read_only and not logged_in}}
33 33 data-notebook-id={{notebook_id}}
34 34
35 35 {% end %}
36 36
37 37
38 38 {% block header %}
39 39
40 40 <span id="save_widget">
41 41 <span id="notebook_name"></span>
42 42 <span id="save_status"></span>
43 43 </span>
44 44
45 45 {% end %}
46 46
47 47
48 48 {% block site %}
49 49
50 50 <div id="menubar_container">
51 51 <div id="menubar">
52 52 <ul id="menus">
53 53 <li><a href="#">File</a>
54 54 <ul>
55 55 <li id="new_notebook"><a href="#">New</a></li>
56 56 <li id="open_notebook"><a href="#">Open...</a></li>
57 57 <hr/>
58 58 <li id="copy_notebook"><a href="#">Make a Copy...</a></li>
59 59 <li id="rename_notebook"><a href="#">Rename...</a></li>
60 60 <li id="save_notebook"><a href="#">Save</a></li>
61 61 <hr/>
62 62 <li><a href="#">Download as</a>
63 63 <ul>
64 64 <li id="download_ipynb"><a href="#">IPython (.ipynb)</a></li>
65 65 <li id="download_py"><a href="#">Python (.py)</a></li>
66 66 </ul>
67 67 </li>
68 68 <hr/>
69 69 <li id="print_notebook"><a href="/{{notebook_id}}/print" target="_blank">Print View</a></li>
70 70 <hr/>
71 71 <li id="kill_and_exit"><a href="#" >Close and halt</a></li>
72 72 </ul>
73 73 </li>
74 74 <li><a href="#">Edit</a>
75 75 <ul>
76 76 <li id="cut_cell"><a href="#">Cut Cell</a></li>
77 77 <li id="copy_cell"><a href="#">Copy Cell</a></li>
78 78 <li id="paste_cell" class="ui-state-disabled"><a href="#">Paste Cell</a></li>
79 79 <li id="paste_cell_above" class="ui-state-disabled"><a href="#">Paste Cell Above</a></li>
80 80 <li id="paste_cell_below" class="ui-state-disabled"><a href="#">Paste Cell Below</a></li>
81 81 <li id="delete_cell"><a href="#">Delete</a></li>
82 82 <hr/>
83 83 <li id="split_cell"><a href="#">Split Cell</a></li>
84 84 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
85 85 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
86 86 <hr/>
87 87 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
88 88 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
89 89 <hr/>
90 90 <li id="select_previous"><a href="#">Select Previous Cell</a></li>
91 91 <li id="select_next"><a href="#">Select Next Cell</a></li>
92 92 </ul>
93 93 </li>
94 94 <li><a href="#">View</a>
95 95 <ul>
96 96 <li id="toggle_header"><a href="#">Toggle Header</a></li>
97 97 <li id="toggle_toolbar"><a href="#">Toggle Toolbar</a></li>
98 98 </ul>
99 99 </li>
100 100 <li><a href="#">Insert</a>
101 101 <ul>
102 102 <li id="insert_cell_above"><a href="#">Insert Cell Above</a></li>
103 103 <li id="insert_cell_below"><a href="#">Insert Cell Below</a></li>
104 104 </ul>
105 105 </li>
106 106 <li><a href="#">Cell</a>
107 107 <ul>
108 108 <li id="run_cell"><a href="#">Run</a></li>
109 109 <li id="run_cell_in_place"><a href="#">Run in Place</a></li>
110 110 <li id="run_all_cells"><a href="#">Run All</a></li>
111 111 <hr/>
112 112 <li id="to_code"><a href="#">Code</a></li>
113 113 <li id="to_markdown"><a href="#">Markdown </a></li>
114 114 <li id="to_raw"><a href="#">Raw Text</a></li>
115 115 <li id="to_heading1"><a href="#">Heading 1</a></li>
116 116 <li id="to_heading2"><a href="#">Heading 2</a></li>
117 117 <li id="to_heading3"><a href="#">Heading 3</a></li>
118 118 <li id="to_heading4"><a href="#">Heading 4</a></li>
119 119 <li id="to_heading5"><a href="#">Heading 5</a></li>
120 120 <li id="to_heading6"><a href="#">Heading 6</a></li>
121 121 <hr/>
122 122 <li id="toggle_output"><a href="#">Toggle Current Output</a></li>
123 123 <li id="all_outputs"><a href="#">All Output</a>
124 124 <ul>
125 125 <li id="expand_all_output"><a href="#">Expand</a></li>
126 126 <li id="scroll_all_output"><a href="#">Scroll Long</a></li>
127 127 <li id="collapse_all_output"><a href="#">Collapse</a></li>
128 128 <li id="clear_all_output"><a href="#">Clear</a></li>
129 129 </ul>
130 130 </li>
131 131 </ul>
132 132 </li>
133 133 <li><a href="#">Kernel</a>
134 134 <ul>
135 135 <li id="int_kernel"><a href="#">Interrupt</a></li>
136 136 <li id="restart_kernel"><a href="#">Restart</a></li>
137 137 </ul>
138 138 </li>
139 139 <li><a href="#">Help</a>
140 140 <ul>
141 141 <li><a href="http://ipython.org/documentation.html" target="_blank">IPython Help</a></li>
142 142 <li><a href="http://ipython.org/ipython-doc/stable/interactive/htmlnotebook.html" target="_blank">Notebook Help</a></li>
143 143 <li id="keyboard_shortcuts"><a href="#">Keyboard Shortcuts</a></li>
144 144 <hr/>
145 145 <li><a href="http://docs.python.org" target="_blank">Python</a></li>
146 146 <li><a href="http://docs.scipy.org/doc/numpy/reference/" target="_blank">NumPy</a></li>
147 147 <li><a href="http://docs.scipy.org/doc/scipy/reference/" target="_blank">SciPy</a></li>
148 148 <li><a href="http://docs.sympy.org/dev/index.html" target="_blank">SymPy</a></li>
149 149 <li><a href="http://matplotlib.sourceforge.net/" target="_blank">Matplotlib</a></li>
150 150 </ul>
151 151 </li>
152 152 </ul>
153 153
154 154 </div>
155 155 <div id="notification"></div>
156 156 </div>
157 157
158 158
159 159 <div id="toolbar">
160 160
161 161 <span>
162 162 <button id="save_b">Save</button>
163 163 </span>
164 164 <span id="cut_copy_paste">
165 165 <button id="cut_b" title="Cut Cell">Cut Cell</button>
166 166 <button id="copy_b" title="Copy Cell">Copy Cell</button>
167 167 <button id="paste_b" title="Paste Cell">Paste Cell</button>
168 168 </span>
169 169 <span id="move_up_down">
170 170 <button id="move_up_b" title="Move Cell Up">Move Cell Up</button>
171 171 <button id="move_down_b" title="Move Cell Down">Move Down</button>
172 172 </span>
173 173 <span id="insert_above_below">
174 174 <button id="insert_above_b" title="Insert Cell Above">Insert Cell Above</button>
175 175 <button id="insert_below_b" title="Insert Cell Below">Insert Cell Below</button>
176 176 </span>
177 177 <span id="run_int">
178 178 <button id="run_b" title="Run Cell">Run Cell</button>
179 179 <button id="interrupt_b" title="Interrupt">Interrupt</button>
180 180 </span>
181 181 <span>
182 182 <select id="cell_type">
183 183 <option value="code">Code</option>
184 184 <option value="markdown">Markdown</option>
185 185 <option value="raw">Raw Text</option>
186 186 <option value="heading1">Heading 1</option>
187 187 <option value="heading2">Heading 2</option>
188 188 <option value="heading3">Heading 3</option>
189 189 <option value="heading4">Heading 4</option>
190 190 <option value="heading5">Heading 5</option>
191 191 <option value="heading6">Heading 6</option>
192 192 </select>
193 193 </span>
194 194
195 195 </div>
196 196
197 197 <div id="main_app">
198 198
199 199 <div id="notebook_panel">
200 200 <div id="notebook"></div>
201 201 <div id="pager_splitter"></div>
202 202 <div id="pager"></div>
203 203 </div>
204 204
205 205 </div>
206 206 <div id='tooltip' class='tooltip ui-corner-all' style='display:none'></div>
207 207
208 208
209 209 {% end %}
210 210
211 211
212 212 {% block script %}
213 213
214 214 <script src="{{ static_url("codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
215 215 <script src="{{ static_url("codemirror/mode/python/python.js") }}" charset="utf-8"></script>
216 216 <script src="{{ static_url("codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
217 217 <script src="{{ static_url("codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
218 218 <script src="{{ static_url("codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
219 219 <script src="{{ static_url("codemirror/mode/css/css.js") }}" charset="utf-8"></script>
220 220 <script src="{{ static_url("codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
221 221 <script src="{{ static_url("codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
222 222
223 223 <script src="{{ static_url("pagedown/Markdown.Converter.js") }}" charset="utf-8"></script>
224 224
225 225 <script src="{{ static_url("prettify/prettify.js") }}" charset="utf-8"></script>
226 226 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
227 227
228 228 <script src="{{ static_url("js/events.js") }}" type="text/javascript" charset="utf-8"></script>
229 229 <script src="{{ static_url("js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
230 230 <script src="{{ static_url("js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
231 231 <script src="{{ static_url("js/initmathjax.js") }}" type="text/javascript" charset="utf-8"></script>
232 232 <script src="{{ static_url("js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
233 233 <script src="{{ static_url("js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
234 234 <script src="{{ static_url("js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
235 235 <script src="{{ static_url("js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
236 236 <script src="{{ static_url("js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
237 237 <script src="{{ static_url("js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
238 238 <script src="{{ static_url("js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
239 239 <script src="{{ static_url("js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
240 240 <script src="{{ static_url("js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
241 241 <script src="{{ static_url("js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
242 242 <script src="{{ static_url("js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
243 243 <script src="{{ static_url("js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
244 244 <script src="{{ static_url("js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
245 245 <script src="{{ static_url("js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
246 246 <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
247 247
248 <script src="{{ static_url("js/contexthint.js") }} charset="utf-8"></script>
248 <script src="{{ static_url("js/contexthint.js") }}" charset="utf-8"></script>
249 249
250 250 {% end %}
@@ -1,58 +1,62
1 1 <!DOCTYPE HTML>
2 2 <html>
3 3
4 4 <head>
5 5 <meta charset="utf-8">
6 6
7 7 <title>{% block title %}IPython Notebook{% end %}</title>
8 8
9 9 <link rel="stylesheet" href="{{static_url("jquery/css/themes/base/jquery-ui.min.css") }}" type="text/css" />
10 10 <link rel="stylesheet" href="{{static_url("css/boilerplate.css") }}" type="text/css" />
11 11 <link rel="stylesheet" href="{{static_url("css/fbm.css") }}" type="text/css" />
12 12 <link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
13 13 {% block stylesheet %}
14 14 {% end %}
15 <link rel="stylesheet" href="{{ static_url("css/custom.css") }}" type="text/css" />
16
15 17
16 18 {% block meta %}
17 19 {% end %}
18 20
19 21 </head>
20 22
21 23 <body {% block params %}{% end %}>
22 24
23 25 <div id="header">
24 26 <span id="ipython_notebook"><h1><a href={{base_project_url}} alt='dashboard'><img src='{{static_url("ipynblogo.png") }}' alt='IPython Notebook'/></a></h1></span>
25 27
26 28 {% block login_widget %}
27 29
28 30 <span id="login_widget">
29 31 {% if logged_in %}
30 32 <button id="logout">Logout</button>
31 33 {% elif login_available and not logged_in %}
32 34 <button id="login">Login</button>
33 35 {% end %}
34 36 </span>
35 37
36 38 {% end %}
37 39
38 40 {% block header %}
39 41 {% end %}
40 42 </div>
41 43
42 44 <div id="site">
43 45 {% block site %}
44 46 {% end %}
45 47 </div>
46 48
47 49 <script src="{{static_url("jquery/js/jquery-1.7.1.min.js") }}" type="text/javascript" charset="utf-8"></script>
48 50 <script src="{{static_url("jquery/js/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
49 51 <script src="{{static_url("js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
50 52 <script src="{{static_url("js/page.js") }}" type="text/javascript" charset="utf-8"></script>
51 53 <script src="{{static_url("js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
52 54
53 55 {% block script %}
54 56 {% end %}
55 57
58 <script src="{{static_url("js/custom.js") }}" type="text/javascript" charset="utf-8"></script>
59
56 60 </body>
57 61
58 62 </html>
General Comments 0
You need to be logged in to leave comments. Login now