##// END OF EJS Templates
Add info, error and warning message boxes.
Stefan van der Walt -
Show More
@@ -1,576 +1,576 b''
1 1 """Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import logging
20 20 import Cookie
21 21 import uuid
22 22
23 23 from tornado import web
24 24 from tornado import websocket
25 25
26 26 from zmq.eventloop import ioloop
27 27 from zmq.utils import jsonapi
28 28
29 29 from IPython.external.decorator import decorator
30 30 from IPython.zmq.session import Session
31 31 from IPython.lib.security import passwd_check
32 32
33 33 try:
34 34 from docutils.core import publish_string
35 35 except ImportError:
36 36 publish_string = None
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 40 #-----------------------------------------------------------------------------
41 41
42 42 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 43 # parts tornado cares about haven't really changed, so it's OK to continue
44 44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 45 # version as of Oct 30/2011) the version check fails, see the issue report:
46 46
47 47 # https://github.com/facebook/tornado/issues/385
48 48
49 49 # This issue has been fixed in Tornado post 2.1.1:
50 50
51 51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52 52
53 53 # Here we manually apply the same patch as above so that users of IPython can
54 54 # continue to work with an officially released Tornado. We make the
55 55 # monkeypatch version check as narrow as possible to limit its effects; once
56 56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57 57
58 58 import tornado
59 59
60 60 if tornado.version_info <= (2,1,1):
61 61
62 62 def _execute(self, transforms, *args, **kwargs):
63 63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64 64
65 65 self.open_args = args
66 66 self.open_kwargs = kwargs
67 67
68 68 # The difference between version 8 and 13 is that in 8 the
69 69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 70 # simply "Origin".
71 71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 72 self.ws_connection = WebSocketProtocol8(self)
73 73 self.ws_connection.accept_connection()
74 74
75 75 elif self.request.headers.get("Sec-WebSocket-Version"):
76 76 self.stream.write(tornado.escape.utf8(
77 77 "HTTP/1.1 426 Upgrade Required\r\n"
78 78 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 79 self.stream.close()
80 80
81 81 else:
82 82 self.ws_connection = WebSocketProtocol76(self)
83 83 self.ws_connection.accept_connection()
84 84
85 85 websocket.WebSocketHandler._execute = _execute
86 86 del _execute
87 87
88 88 #-----------------------------------------------------------------------------
89 89 # Decorator for disabling read-only handlers
90 90 #-----------------------------------------------------------------------------
91 91
92 92 @decorator
93 93 def not_if_readonly(f, self, *args, **kwargs):
94 94 if self.application.read_only:
95 95 raise web.HTTPError(403, "Notebook server is read-only")
96 96 else:
97 97 return f(self, *args, **kwargs)
98 98
99 99 @decorator
100 100 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 101 """authenticate this page *unless* readonly view is active.
102 102
103 103 In read-only mode, the notebook list and print view should
104 104 be accessible without authentication.
105 105 """
106 106
107 107 @web.authenticated
108 108 def auth_f(self, *args, **kwargs):
109 109 return f(self, *args, **kwargs)
110 110 if self.application.read_only:
111 111 return f(self, *args, **kwargs)
112 112 else:
113 113 return auth_f(self, *args, **kwargs)
114 114
115 115 #-----------------------------------------------------------------------------
116 116 # Top-level handlers
117 117 #-----------------------------------------------------------------------------
118 118
119 119 class RequestHandler(web.RequestHandler):
120 120 """RequestHandler with default variable setting."""
121 121
122 122 def render(*args, **kwargs):
123 123 kwargs.setdefault('message', '')
124 124 return web.RequestHandler.render(*args, **kwargs)
125 125
126 126 class AuthenticatedHandler(RequestHandler):
127 127 """A RequestHandler with an authenticated user."""
128 128
129 129 def get_current_user(self):
130 130 user_id = self.get_secure_cookie("username")
131 131 # For now the user_id should not return empty, but it could eventually
132 132 if user_id == '':
133 133 user_id = 'anonymous'
134 134 if user_id is None:
135 135 # prevent extra Invalid cookie sig warnings:
136 136 self.clear_cookie('username')
137 137 if not self.application.password and not self.application.read_only:
138 138 user_id = 'anonymous'
139 139 return user_id
140 140
141 141 @property
142 142 def read_only(self):
143 143 if self.application.read_only:
144 144 if self.application.password:
145 145 return self.get_current_user() is None
146 146 else:
147 147 return True
148 148 else:
149 149 return False
150 150
151 151 @property
152 152 def ws_url(self):
153 153 """websocket url matching the current request
154 154
155 155 turns http[s]://host[:port] into
156 156 ws[s]://host[:port]
157 157 """
158 158 proto = self.request.protocol.replace('http', 'ws')
159 159 return "%s://%s" % (proto, self.request.host)
160 160
161 161
162 162 class ProjectDashboardHandler(AuthenticatedHandler):
163 163
164 164 @authenticate_unless_readonly
165 165 def get(self):
166 166 nbm = self.application.notebook_manager
167 167 project = nbm.notebook_dir
168 168 self.render(
169 169 'projectdashboard.html', project=project,
170 170 base_project_url=u'/', base_kernel_url=u'/',
171 171 read_only=self.read_only,
172 172 )
173 173
174 174
175 175 class LoginHandler(AuthenticatedHandler):
176 176
177 def _render(self, message=''):
177 def _render(self, message=None):
178 178 self.render('login.html',
179 179 next=self.get_argument('next', default='/'),
180 180 read_only=self.read_only,
181 181 message=message
182 182 )
183 183
184 184 def get(self):
185 185 self._render()
186 186
187 187 def post(self):
188 188 pwd = self.get_argument('password', default=u'')
189 189 if self.application.password:
190 190 if passwd_check(self.application.password, pwd):
191 191 self.set_secure_cookie('username', str(uuid.uuid4()))
192 192 else:
193 self._render(message='Invalid password')
193 self._render(message={'error': 'Invalid password'})
194 194 return
195 195
196 196 self.redirect(self.get_argument('next', default='/'))
197 197
198 198
199 199 class LogoutHandler(AuthenticatedHandler):
200 200
201 201 def get(self):
202 202 self.clear_cookie('username')
203 self.render('logout.html')
203 self.render('logout.html', message={'info': 'Successfully logged out.'})
204 204
205 205
206 206 class NewHandler(AuthenticatedHandler):
207 207
208 208 @web.authenticated
209 209 def get(self):
210 210 nbm = self.application.notebook_manager
211 211 project = nbm.notebook_dir
212 212 notebook_id = nbm.new_notebook()
213 213 self.render(
214 214 'notebook.html', project=project,
215 215 notebook_id=notebook_id,
216 216 base_project_url=u'/', base_kernel_url=u'/',
217 217 kill_kernel=False,
218 218 read_only=False,
219 219 )
220 220
221 221
222 222 class NamedNotebookHandler(AuthenticatedHandler):
223 223
224 224 @authenticate_unless_readonly
225 225 def get(self, notebook_id):
226 226 nbm = self.application.notebook_manager
227 227 project = nbm.notebook_dir
228 228 if not nbm.notebook_exists(notebook_id):
229 229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
230 230
231 231 self.render(
232 232 'notebook.html', project=project,
233 233 notebook_id=notebook_id,
234 234 base_project_url=u'/', base_kernel_url=u'/',
235 235 kill_kernel=False,
236 236 read_only=self.read_only,
237 237 )
238 238
239 239
240 240 #-----------------------------------------------------------------------------
241 241 # Kernel handlers
242 242 #-----------------------------------------------------------------------------
243 243
244 244
245 245 class MainKernelHandler(AuthenticatedHandler):
246 246
247 247 @web.authenticated
248 248 def get(self):
249 249 km = self.application.kernel_manager
250 250 self.finish(jsonapi.dumps(km.kernel_ids))
251 251
252 252 @web.authenticated
253 253 def post(self):
254 254 km = self.application.kernel_manager
255 255 notebook_id = self.get_argument('notebook', default=None)
256 256 kernel_id = km.start_kernel(notebook_id)
257 257 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
258 258 self.set_header('Location', '/'+kernel_id)
259 259 self.finish(jsonapi.dumps(data))
260 260
261 261
262 262 class KernelHandler(AuthenticatedHandler):
263 263
264 264 SUPPORTED_METHODS = ('DELETE')
265 265
266 266 @web.authenticated
267 267 def delete(self, kernel_id):
268 268 km = self.application.kernel_manager
269 269 km.kill_kernel(kernel_id)
270 270 self.set_status(204)
271 271 self.finish()
272 272
273 273
274 274 class KernelActionHandler(AuthenticatedHandler):
275 275
276 276 @web.authenticated
277 277 def post(self, kernel_id, action):
278 278 km = self.application.kernel_manager
279 279 if action == 'interrupt':
280 280 km.interrupt_kernel(kernel_id)
281 281 self.set_status(204)
282 282 if action == 'restart':
283 283 new_kernel_id = km.restart_kernel(kernel_id)
284 284 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
285 285 self.set_header('Location', '/'+new_kernel_id)
286 286 self.write(jsonapi.dumps(data))
287 287 self.finish()
288 288
289 289
290 290 class ZMQStreamHandler(websocket.WebSocketHandler):
291 291
292 292 def _reserialize_reply(self, msg_list):
293 293 """Reserialize a reply message using JSON.
294 294
295 295 This takes the msg list from the ZMQ socket, unserializes it using
296 296 self.session and then serializes the result using JSON. This method
297 297 should be used by self._on_zmq_reply to build messages that can
298 298 be sent back to the browser.
299 299 """
300 300 idents, msg_list = self.session.feed_identities(msg_list)
301 301 msg = self.session.unserialize(msg_list)
302 302 try:
303 303 msg['header'].pop('date')
304 304 except KeyError:
305 305 pass
306 306 try:
307 307 msg['parent_header'].pop('date')
308 308 except KeyError:
309 309 pass
310 310 msg.pop('buffers')
311 311 return jsonapi.dumps(msg)
312 312
313 313 def _on_zmq_reply(self, msg_list):
314 314 try:
315 315 msg = self._reserialize_reply(msg_list)
316 316 except:
317 317 self.application.log.critical("Malformed message: %r" % msg_list)
318 318 else:
319 319 self.write_message(msg)
320 320
321 321
322 322 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
323 323
324 324 def open(self, kernel_id):
325 325 self.kernel_id = kernel_id.decode('ascii')
326 326 try:
327 327 cfg = self.application.ipython_app.config
328 328 except AttributeError:
329 329 # protect from the case where this is run from something other than
330 330 # the notebook app:
331 331 cfg = None
332 332 self.session = Session(config=cfg)
333 333 self.save_on_message = self.on_message
334 334 self.on_message = self.on_first_message
335 335
336 336 def get_current_user(self):
337 337 user_id = self.get_secure_cookie("username")
338 338 if user_id == '' or (user_id is None and not self.application.password):
339 339 user_id = 'anonymous'
340 340 return user_id
341 341
342 342 def _inject_cookie_message(self, msg):
343 343 """Inject the first message, which is the document cookie,
344 344 for authentication."""
345 345 if isinstance(msg, unicode):
346 346 # Cookie can't constructor doesn't accept unicode strings for some reason
347 347 msg = msg.encode('utf8', 'replace')
348 348 try:
349 349 self.request._cookies = Cookie.SimpleCookie(msg)
350 350 except:
351 351 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
352 352
353 353 def on_first_message(self, msg):
354 354 self._inject_cookie_message(msg)
355 355 if self.get_current_user() is None:
356 356 logging.warn("Couldn't authenticate WebSocket connection")
357 357 raise web.HTTPError(403)
358 358 self.on_message = self.save_on_message
359 359
360 360
361 361 class IOPubHandler(AuthenticatedZMQStreamHandler):
362 362
363 363 def initialize(self, *args, **kwargs):
364 364 self._kernel_alive = True
365 365 self._beating = False
366 366 self.iopub_stream = None
367 367 self.hb_stream = None
368 368
369 369 def on_first_message(self, msg):
370 370 try:
371 371 super(IOPubHandler, self).on_first_message(msg)
372 372 except web.HTTPError:
373 373 self.close()
374 374 return
375 375 km = self.application.kernel_manager
376 376 self.time_to_dead = km.time_to_dead
377 377 kernel_id = self.kernel_id
378 378 try:
379 379 self.iopub_stream = km.create_iopub_stream(kernel_id)
380 380 self.hb_stream = km.create_hb_stream(kernel_id)
381 381 except web.HTTPError:
382 382 # WebSockets don't response to traditional error codes so we
383 383 # close the connection.
384 384 if not self.stream.closed():
385 385 self.stream.close()
386 386 self.close()
387 387 else:
388 388 self.iopub_stream.on_recv(self._on_zmq_reply)
389 389 self.start_hb(self.kernel_died)
390 390
391 391 def on_message(self, msg):
392 392 pass
393 393
394 394 def on_close(self):
395 395 # This method can be called twice, once by self.kernel_died and once
396 396 # from the WebSocket close event. If the WebSocket connection is
397 397 # closed before the ZMQ streams are setup, they could be None.
398 398 self.stop_hb()
399 399 if self.iopub_stream is not None and not self.iopub_stream.closed():
400 400 self.iopub_stream.on_recv(None)
401 401 self.iopub_stream.close()
402 402 if self.hb_stream is not None and not self.hb_stream.closed():
403 403 self.hb_stream.close()
404 404
405 405 def start_hb(self, callback):
406 406 """Start the heartbeating and call the callback if the kernel dies."""
407 407 if not self._beating:
408 408 self._kernel_alive = True
409 409
410 410 def ping_or_dead():
411 411 if self._kernel_alive:
412 412 self._kernel_alive = False
413 413 self.hb_stream.send(b'ping')
414 414 else:
415 415 try:
416 416 callback()
417 417 except:
418 418 pass
419 419 finally:
420 420 self._hb_periodic_callback.stop()
421 421
422 422 def beat_received(msg):
423 423 self._kernel_alive = True
424 424
425 425 self.hb_stream.on_recv(beat_received)
426 426 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
427 427 self._hb_periodic_callback.start()
428 428 self._beating= True
429 429
430 430 def stop_hb(self):
431 431 """Stop the heartbeating and cancel all related callbacks."""
432 432 if self._beating:
433 433 self._hb_periodic_callback.stop()
434 434 if not self.hb_stream.closed():
435 435 self.hb_stream.on_recv(None)
436 436
437 437 def kernel_died(self):
438 438 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
439 439 self.write_message(
440 440 {'header': {'msg_type': 'status'},
441 441 'parent_header': {},
442 442 'content': {'execution_state':'dead'}
443 443 }
444 444 )
445 445 self.on_close()
446 446
447 447
448 448 class ShellHandler(AuthenticatedZMQStreamHandler):
449 449
450 450 def initialize(self, *args, **kwargs):
451 451 self.shell_stream = None
452 452
453 453 def on_first_message(self, msg):
454 454 try:
455 455 super(ShellHandler, self).on_first_message(msg)
456 456 except web.HTTPError:
457 457 self.close()
458 458 return
459 459 km = self.application.kernel_manager
460 460 self.max_msg_size = km.max_msg_size
461 461 kernel_id = self.kernel_id
462 462 try:
463 463 self.shell_stream = km.create_shell_stream(kernel_id)
464 464 except web.HTTPError:
465 465 # WebSockets don't response to traditional error codes so we
466 466 # close the connection.
467 467 if not self.stream.closed():
468 468 self.stream.close()
469 469 self.close()
470 470 else:
471 471 self.shell_stream.on_recv(self._on_zmq_reply)
472 472
473 473 def on_message(self, msg):
474 474 if len(msg) < self.max_msg_size:
475 475 msg = jsonapi.loads(msg)
476 476 self.session.send(self.shell_stream, msg)
477 477
478 478 def on_close(self):
479 479 # Make sure the stream exists and is not already closed.
480 480 if self.shell_stream is not None and not self.shell_stream.closed():
481 481 self.shell_stream.close()
482 482
483 483
484 484 #-----------------------------------------------------------------------------
485 485 # Notebook web service handlers
486 486 #-----------------------------------------------------------------------------
487 487
488 488 class NotebookRootHandler(AuthenticatedHandler):
489 489
490 490 @authenticate_unless_readonly
491 491 def get(self):
492 492
493 493 nbm = self.application.notebook_manager
494 494 files = nbm.list_notebooks()
495 495 self.finish(jsonapi.dumps(files))
496 496
497 497 @web.authenticated
498 498 def post(self):
499 499 nbm = self.application.notebook_manager
500 500 body = self.request.body.strip()
501 501 format = self.get_argument('format', default='json')
502 502 name = self.get_argument('name', default=None)
503 503 if body:
504 504 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
505 505 else:
506 506 notebook_id = nbm.new_notebook()
507 507 self.set_header('Location', '/'+notebook_id)
508 508 self.finish(jsonapi.dumps(notebook_id))
509 509
510 510
511 511 class NotebookHandler(AuthenticatedHandler):
512 512
513 513 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
514 514
515 515 @authenticate_unless_readonly
516 516 def get(self, notebook_id):
517 517 nbm = self.application.notebook_manager
518 518 format = self.get_argument('format', default='json')
519 519 last_mod, name, data = nbm.get_notebook(notebook_id, format)
520 520
521 521 if format == u'json':
522 522 self.set_header('Content-Type', 'application/json')
523 523 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
524 524 elif format == u'py':
525 525 self.set_header('Content-Type', 'application/x-python')
526 526 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
527 527 self.set_header('Last-Modified', last_mod)
528 528 self.finish(data)
529 529
530 530 @web.authenticated
531 531 def put(self, notebook_id):
532 532 nbm = self.application.notebook_manager
533 533 format = self.get_argument('format', default='json')
534 534 name = self.get_argument('name', default=None)
535 535 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
536 536 self.set_status(204)
537 537 self.finish()
538 538
539 539 @web.authenticated
540 540 def delete(self, notebook_id):
541 541 nbm = self.application.notebook_manager
542 542 nbm.delete_notebook(notebook_id)
543 543 self.set_status(204)
544 544 self.finish()
545 545
546 546 #-----------------------------------------------------------------------------
547 547 # RST web service handlers
548 548 #-----------------------------------------------------------------------------
549 549
550 550
551 551 class RSTHandler(AuthenticatedHandler):
552 552
553 553 @web.authenticated
554 554 def post(self):
555 555 if publish_string is None:
556 556 raise web.HTTPError(503, u'docutils not available')
557 557 body = self.request.body.strip()
558 558 source = body
559 559 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
560 560 defaults = {'file_insertion_enabled': 0,
561 561 'raw_enabled': 0,
562 562 '_disable_config': 1,
563 563 'stylesheet_path': 0
564 564 # 'template': template_path
565 565 }
566 566 try:
567 567 html = publish_string(source, writer_name='html',
568 568 settings_overrides=defaults
569 569 )
570 570 except:
571 571 raise web.HTTPError(400, u'Invalid RST')
572 572 print html
573 573 self.set_header('Content-Type', 'text/html')
574 574 self.finish(html)
575 575
576 576
@@ -1,115 +1,130 b''
1 1
2 2 .border-box-sizing {
3 3 box-sizing: border-box;
4 4 -moz-box-sizing: border-box;
5 5 -webkit-box-sizing: border-box;
6 6 }
7 7
8 8 /* Flexible box model classes */
9 9 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
10 10
11 11 .hbox {
12 12 display: -webkit-box;
13 13 -webkit-box-orient: horizontal;
14 14 -webkit-box-align: stretch;
15 15
16 16 display: -moz-box;
17 17 -moz-box-orient: horizontal;
18 18 -moz-box-align: stretch;
19 19
20 20 display: box;
21 21 box-orient: horizontal;
22 22 box-align: stretch;
23 23 }
24 24
25 25 .hbox > * {
26 26 -webkit-box-flex: 0;
27 27 -moz-box-flex: 0;
28 28 box-flex: 0;
29 29 }
30 30
31 31 .vbox {
32 32 display: -webkit-box;
33 33 -webkit-box-orient: vertical;
34 34 -webkit-box-align: stretch;
35 35
36 36 display: -moz-box;
37 37 -moz-box-orient: vertical;
38 38 -moz-box-align: stretch;
39 39
40 40 display: box;
41 41 box-orient: vertical;
42 42 box-align: stretch;
43 43 }
44 44
45 45 .vbox > * {
46 46 -webkit-box-flex: 0;
47 47 -moz-box-flex: 0;
48 48 box-flex: 0;
49 49 }
50 50
51 51 .reverse {
52 52 -webkit-box-direction: reverse;
53 53 -moz-box-direction: reverse;
54 54 box-direction: reverse;
55 55 }
56 56
57 57 .box-flex0 {
58 58 -webkit-box-flex: 0;
59 59 -moz-box-flex: 0;
60 60 box-flex: 0;
61 61 }
62 62
63 63 .box-flex1, .box-flex {
64 64 -webkit-box-flex: 1;
65 65 -moz-box-flex: 1;
66 66 box-flex: 1;
67 67 }
68 68
69 69 .box-flex2 {
70 70 -webkit-box-flex: 2;
71 71 -moz-box-flex: 2;
72 72 box-flex: 2;
73 73 }
74 74
75 75 .box-group1 {
76 76 -webkit-box-flex-group: 1;
77 77 -moz-box-flex-group: 1;
78 78 box-flex-group: 1;
79 79 }
80 80
81 81 .box-group2 {
82 82 -webkit-box-flex-group: 2;
83 83 -moz-box-flex-group: 2;
84 84 box-flex-group: 2;
85 85 }
86 86
87 87 .start {
88 88 -webkit-box-pack: start;
89 89 -moz-box-pack: start;
90 90 box-pack: start;
91 91 }
92 92
93 93 .end {
94 94 -webkit-box-pack: end;
95 95 -moz-box-pack: end;
96 96 box-pack: end;
97 97 }
98 98
99 99 .center {
100 100 -webkit-box-pack: center;
101 101 -moz-box-pack: center;
102 102 box-pack: center;
103 103 }
104 104
105 #message {
106 border: 1px solid red;
107 background-color: #FFD3D1;
105 .message {
106 border-width: 1px;
107 border-style: solid;
108 108 text-align: center;
109 109 padding: 0.5em;
110 margin: 0.5em;
110 margin: 0.5em 0;
111 }
112
113 .message.error {
114 background-color: #FFD3D1;
115 border-color: red;
116 }
117
118 .message.warning {
119 background-color: #FFD09E;
120 border-color: orange;
121 }
122
123 .message.info {
124 background-color: #CBFFBA;
125 border-color: green;
111 126 }
112 127
113 128 #content_panel {
114 129 margin: 0.5em;
115 130 } No newline at end of file
@@ -1,72 +1,75 b''
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/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 12 <link rel="stylesheet" href="static/css/base.css" type="text/css"/>
13 13 {% block stylesheet %}
14 14 {% end %}
15 15
16 16 {% block meta %}
17 17 {% end %}
18 18
19 19 </head>
20 20
21 21 <body {% block params %}{% end %}>
22 22
23 23 <div id="header">
24 24 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
25 25 <span id="login_widget">
26 26 <button id="logout">Logout</button>
27 27 </span>
28 28 {% block header %}
29 29 {% end %}
30 30 </div>
31 31
32 32 <div id="header_border"></div>
33 33
34 34 <div id="main_app">
35 35
36 36 <div id="app_hbox">
37 37
38 38 <div id="left_panel">
39 39 {% block left_panel %}
40 40 {% end %}
41 41 </div>
42 42
43 43 <div id="content_panel">
44 44 {% if message %}
45 <div id="message">
46 {{message}}
47 </div>
45
46 {% for key in message %}
47 <div class="message {{key}}">
48 {{message[key]}}
49 </div>
50 {% end %}
48 51 {% end %}
49 52
50 53 {% block content_panel %}
51 54 {% end %}
52 55 </div>
53 56 <div id="right_panel">
54 57 {% block right_panel %}
55 58 {% end %}
56 59 </div>
57 60
58 61 </div>
59 62
60 63 </div>
61 64
62 65 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
63 66 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
64 67 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
65 68 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
66 69 <script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
67 70 {% block script %}
68 71 {% end %}
69 72
70 73 </body>
71 74
72 75 </html>
@@ -1,5 +1,5 b''
1 1 {% extends layout.html %}
2 2
3 3 {% block content_panel %}
4 You've been successfully logged out.
4 Proceed to the <a href="/login">login page</a>.
5 5 {% end %}
General Comments 0
You need to be logged in to leave comments. Login now