##// END OF EJS Templates
Use template inheritance.
Stefan van der Walt -
Show More
@@ -0,0 +1,71 b''
1 <!DOCTYPE HTML>
2 <html>
3
4 <head>
5 <meta charset="utf-8">
6
7 <title>{% block title %}IPython Notebook{% end %}</title>
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css"/>
13 {% block stylesheet %}
14 {% end %}
15
16 <meta name="read_only" content="{{read_only}}"/>
17
18 </head>
19
20 <body>
21
22 <div id="header">
23 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
24 <span id="login_widget" class="hidden">
25 <button id="login">Login</button>
26 </span>
27 {% block header %}
28 {% end %}
29 </div>
30
31 <div id="header_border"></div>
32
33 <div id="main_app">
34
35 <div id="app_hbox">
36
37 <div id="left_panel">
38 {% block left_panel %}
39 {% end %}
40 </div>
41
42 <div id="content_panel">
43 {% if message %}
44 <div id="message">
45 {{message}}
46 </div>
47 {% end %}
48
49 {% block content_panel %}
50 {% end %}
51 </div>
52 <div id="right_panel">
53 {% block right_panel %}
54 {% end %}
55 </div>
56
57 </div>
58
59 </div>
60
61 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
62 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
63 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
64 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
65 <script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
66 {% block script %}
67 {% end %}
68
69 </body>
70
71 </html>
@@ -1,562 +1,569 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 class AuthenticatedHandler(web.RequestHandler):
119 class RequestHandler(web.RequestHandler):
120 """RequestHandler with default variable setting."""
121
122 def render(*args, **kwargs):
123 kwargs.setdefault('message', '')
124 return web.RequestHandler.render(*args, **kwargs)
125
126 class AuthenticatedHandler(RequestHandler):
120 127 """A RequestHandler with an authenticated user."""
121 128
122 129 def get_current_user(self):
123 130 user_id = self.get_secure_cookie("username")
124 131 # For now the user_id should not return empty, but it could eventually
125 132 if user_id == '':
126 133 user_id = 'anonymous'
127 134 if user_id is None:
128 135 # prevent extra Invalid cookie sig warnings:
129 136 self.clear_cookie('username')
130 137 if not self.application.password and not self.application.read_only:
131 138 user_id = 'anonymous'
132 139 return user_id
133 140
134 141 @property
135 142 def read_only(self):
136 143 if self.application.read_only:
137 144 if self.application.password:
138 145 return self.get_current_user() is None
139 146 else:
140 147 return True
141 148 else:
142 149 return False
143 150
144 151 @property
145 152 def ws_url(self):
146 153 """websocket url matching the current request
147 154
148 155 turns http[s]://host[:port] into
149 156 ws[s]://host[:port]
150 157 """
151 158 proto = self.request.protocol.replace('http', 'ws')
152 159 return "%s://%s" % (proto, self.request.host)
153 160
154 161
155 162 class ProjectDashboardHandler(AuthenticatedHandler):
156 163
157 164 @authenticate_unless_readonly
158 165 def get(self):
159 166 nbm = self.application.notebook_manager
160 167 project = nbm.notebook_dir
161 168 self.render(
162 169 'projectdashboard.html', project=project,
163 170 base_project_url=u'/', base_kernel_url=u'/',
164 171 read_only=self.read_only,
165 172 )
166 173
167 174
168 175 class LoginHandler(AuthenticatedHandler):
169 176
170 177 def _render(self, message=''):
171 178 self.render('login.html',
172 179 next=self.get_argument('next', default='/'),
173 180 read_only=self.read_only,
174 181 message=message
175 182 )
176 183
177 184 def get(self):
178 185 self._render()
179 186
180 187 def post(self):
181 188 pwd = self.get_argument('password', default=u'')
182 189 if self.application.password:
183 190 if passwd_check(self.application.password, pwd):
184 191 self.set_secure_cookie('username', str(uuid.uuid4()))
185 192 else:
186 193 self._render(message='Invalid password')
187 194 return
188 195
189 196 self.redirect(self.get_argument('next', default='/'))
190 197
191 198
192 199 class NewHandler(AuthenticatedHandler):
193 200
194 201 @web.authenticated
195 202 def get(self):
196 203 nbm = self.application.notebook_manager
197 204 project = nbm.notebook_dir
198 205 notebook_id = nbm.new_notebook()
199 206 self.render(
200 207 'notebook.html', project=project,
201 208 notebook_id=notebook_id,
202 209 base_project_url=u'/', base_kernel_url=u'/',
203 210 kill_kernel=False,
204 211 read_only=False,
205 212 )
206 213
207 214
208 215 class NamedNotebookHandler(AuthenticatedHandler):
209 216
210 217 @authenticate_unless_readonly
211 218 def get(self, notebook_id):
212 219 nbm = self.application.notebook_manager
213 220 project = nbm.notebook_dir
214 221 if not nbm.notebook_exists(notebook_id):
215 222 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
216 223
217 224 self.render(
218 225 'notebook.html', project=project,
219 226 notebook_id=notebook_id,
220 227 base_project_url=u'/', base_kernel_url=u'/',
221 228 kill_kernel=False,
222 229 read_only=self.read_only,
223 230 )
224 231
225 232
226 233 #-----------------------------------------------------------------------------
227 234 # Kernel handlers
228 235 #-----------------------------------------------------------------------------
229 236
230 237
231 238 class MainKernelHandler(AuthenticatedHandler):
232 239
233 240 @web.authenticated
234 241 def get(self):
235 242 km = self.application.kernel_manager
236 243 self.finish(jsonapi.dumps(km.kernel_ids))
237 244
238 245 @web.authenticated
239 246 def post(self):
240 247 km = self.application.kernel_manager
241 248 notebook_id = self.get_argument('notebook', default=None)
242 249 kernel_id = km.start_kernel(notebook_id)
243 250 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
244 251 self.set_header('Location', '/'+kernel_id)
245 252 self.finish(jsonapi.dumps(data))
246 253
247 254
248 255 class KernelHandler(AuthenticatedHandler):
249 256
250 257 SUPPORTED_METHODS = ('DELETE')
251 258
252 259 @web.authenticated
253 260 def delete(self, kernel_id):
254 261 km = self.application.kernel_manager
255 262 km.kill_kernel(kernel_id)
256 263 self.set_status(204)
257 264 self.finish()
258 265
259 266
260 267 class KernelActionHandler(AuthenticatedHandler):
261 268
262 269 @web.authenticated
263 270 def post(self, kernel_id, action):
264 271 km = self.application.kernel_manager
265 272 if action == 'interrupt':
266 273 km.interrupt_kernel(kernel_id)
267 274 self.set_status(204)
268 275 if action == 'restart':
269 276 new_kernel_id = km.restart_kernel(kernel_id)
270 277 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
271 278 self.set_header('Location', '/'+new_kernel_id)
272 279 self.write(jsonapi.dumps(data))
273 280 self.finish()
274 281
275 282
276 283 class ZMQStreamHandler(websocket.WebSocketHandler):
277 284
278 285 def _reserialize_reply(self, msg_list):
279 286 """Reserialize a reply message using JSON.
280 287
281 288 This takes the msg list from the ZMQ socket, unserializes it using
282 289 self.session and then serializes the result using JSON. This method
283 290 should be used by self._on_zmq_reply to build messages that can
284 291 be sent back to the browser.
285 292 """
286 293 idents, msg_list = self.session.feed_identities(msg_list)
287 294 msg = self.session.unserialize(msg_list)
288 295 try:
289 296 msg['header'].pop('date')
290 297 except KeyError:
291 298 pass
292 299 try:
293 300 msg['parent_header'].pop('date')
294 301 except KeyError:
295 302 pass
296 303 msg.pop('buffers')
297 304 return jsonapi.dumps(msg)
298 305
299 306 def _on_zmq_reply(self, msg_list):
300 307 try:
301 308 msg = self._reserialize_reply(msg_list)
302 309 except:
303 310 self.application.log.critical("Malformed message: %r" % msg_list)
304 311 else:
305 312 self.write_message(msg)
306 313
307 314
308 315 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
309 316
310 317 def open(self, kernel_id):
311 318 self.kernel_id = kernel_id.decode('ascii')
312 319 try:
313 320 cfg = self.application.ipython_app.config
314 321 except AttributeError:
315 322 # protect from the case where this is run from something other than
316 323 # the notebook app:
317 324 cfg = None
318 325 self.session = Session(config=cfg)
319 326 self.save_on_message = self.on_message
320 327 self.on_message = self.on_first_message
321 328
322 329 def get_current_user(self):
323 330 user_id = self.get_secure_cookie("username")
324 331 if user_id == '' or (user_id is None and not self.application.password):
325 332 user_id = 'anonymous'
326 333 return user_id
327 334
328 335 def _inject_cookie_message(self, msg):
329 336 """Inject the first message, which is the document cookie,
330 337 for authentication."""
331 338 if isinstance(msg, unicode):
332 339 # Cookie can't constructor doesn't accept unicode strings for some reason
333 340 msg = msg.encode('utf8', 'replace')
334 341 try:
335 342 self.request._cookies = Cookie.SimpleCookie(msg)
336 343 except:
337 344 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
338 345
339 346 def on_first_message(self, msg):
340 347 self._inject_cookie_message(msg)
341 348 if self.get_current_user() is None:
342 349 logging.warn("Couldn't authenticate WebSocket connection")
343 350 raise web.HTTPError(403)
344 351 self.on_message = self.save_on_message
345 352
346 353
347 354 class IOPubHandler(AuthenticatedZMQStreamHandler):
348 355
349 356 def initialize(self, *args, **kwargs):
350 357 self._kernel_alive = True
351 358 self._beating = False
352 359 self.iopub_stream = None
353 360 self.hb_stream = None
354 361
355 362 def on_first_message(self, msg):
356 363 try:
357 364 super(IOPubHandler, self).on_first_message(msg)
358 365 except web.HTTPError:
359 366 self.close()
360 367 return
361 368 km = self.application.kernel_manager
362 369 self.time_to_dead = km.time_to_dead
363 370 kernel_id = self.kernel_id
364 371 try:
365 372 self.iopub_stream = km.create_iopub_stream(kernel_id)
366 373 self.hb_stream = km.create_hb_stream(kernel_id)
367 374 except web.HTTPError:
368 375 # WebSockets don't response to traditional error codes so we
369 376 # close the connection.
370 377 if not self.stream.closed():
371 378 self.stream.close()
372 379 self.close()
373 380 else:
374 381 self.iopub_stream.on_recv(self._on_zmq_reply)
375 382 self.start_hb(self.kernel_died)
376 383
377 384 def on_message(self, msg):
378 385 pass
379 386
380 387 def on_close(self):
381 388 # This method can be called twice, once by self.kernel_died and once
382 389 # from the WebSocket close event. If the WebSocket connection is
383 390 # closed before the ZMQ streams are setup, they could be None.
384 391 self.stop_hb()
385 392 if self.iopub_stream is not None and not self.iopub_stream.closed():
386 393 self.iopub_stream.on_recv(None)
387 394 self.iopub_stream.close()
388 395 if self.hb_stream is not None and not self.hb_stream.closed():
389 396 self.hb_stream.close()
390 397
391 398 def start_hb(self, callback):
392 399 """Start the heartbeating and call the callback if the kernel dies."""
393 400 if not self._beating:
394 401 self._kernel_alive = True
395 402
396 403 def ping_or_dead():
397 404 if self._kernel_alive:
398 405 self._kernel_alive = False
399 406 self.hb_stream.send(b'ping')
400 407 else:
401 408 try:
402 409 callback()
403 410 except:
404 411 pass
405 412 finally:
406 413 self._hb_periodic_callback.stop()
407 414
408 415 def beat_received(msg):
409 416 self._kernel_alive = True
410 417
411 418 self.hb_stream.on_recv(beat_received)
412 419 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
413 420 self._hb_periodic_callback.start()
414 421 self._beating= True
415 422
416 423 def stop_hb(self):
417 424 """Stop the heartbeating and cancel all related callbacks."""
418 425 if self._beating:
419 426 self._hb_periodic_callback.stop()
420 427 if not self.hb_stream.closed():
421 428 self.hb_stream.on_recv(None)
422 429
423 430 def kernel_died(self):
424 431 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
425 432 self.write_message(
426 433 {'header': {'msg_type': 'status'},
427 434 'parent_header': {},
428 435 'content': {'execution_state':'dead'}
429 436 }
430 437 )
431 438 self.on_close()
432 439
433 440
434 441 class ShellHandler(AuthenticatedZMQStreamHandler):
435 442
436 443 def initialize(self, *args, **kwargs):
437 444 self.shell_stream = None
438 445
439 446 def on_first_message(self, msg):
440 447 try:
441 448 super(ShellHandler, self).on_first_message(msg)
442 449 except web.HTTPError:
443 450 self.close()
444 451 return
445 452 km = self.application.kernel_manager
446 453 self.max_msg_size = km.max_msg_size
447 454 kernel_id = self.kernel_id
448 455 try:
449 456 self.shell_stream = km.create_shell_stream(kernel_id)
450 457 except web.HTTPError:
451 458 # WebSockets don't response to traditional error codes so we
452 459 # close the connection.
453 460 if not self.stream.closed():
454 461 self.stream.close()
455 462 self.close()
456 463 else:
457 464 self.shell_stream.on_recv(self._on_zmq_reply)
458 465
459 466 def on_message(self, msg):
460 467 if len(msg) < self.max_msg_size:
461 468 msg = jsonapi.loads(msg)
462 469 self.session.send(self.shell_stream, msg)
463 470
464 471 def on_close(self):
465 472 # Make sure the stream exists and is not already closed.
466 473 if self.shell_stream is not None and not self.shell_stream.closed():
467 474 self.shell_stream.close()
468 475
469 476
470 477 #-----------------------------------------------------------------------------
471 478 # Notebook web service handlers
472 479 #-----------------------------------------------------------------------------
473 480
474 481 class NotebookRootHandler(AuthenticatedHandler):
475 482
476 483 @authenticate_unless_readonly
477 484 def get(self):
478 485
479 486 nbm = self.application.notebook_manager
480 487 files = nbm.list_notebooks()
481 488 self.finish(jsonapi.dumps(files))
482 489
483 490 @web.authenticated
484 491 def post(self):
485 492 nbm = self.application.notebook_manager
486 493 body = self.request.body.strip()
487 494 format = self.get_argument('format', default='json')
488 495 name = self.get_argument('name', default=None)
489 496 if body:
490 497 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
491 498 else:
492 499 notebook_id = nbm.new_notebook()
493 500 self.set_header('Location', '/'+notebook_id)
494 501 self.finish(jsonapi.dumps(notebook_id))
495 502
496 503
497 504 class NotebookHandler(AuthenticatedHandler):
498 505
499 506 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
500 507
501 508 @authenticate_unless_readonly
502 509 def get(self, notebook_id):
503 510 nbm = self.application.notebook_manager
504 511 format = self.get_argument('format', default='json')
505 512 last_mod, name, data = nbm.get_notebook(notebook_id, format)
506 513
507 514 if format == u'json':
508 515 self.set_header('Content-Type', 'application/json')
509 516 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
510 517 elif format == u'py':
511 518 self.set_header('Content-Type', 'application/x-python')
512 519 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
513 520 self.set_header('Last-Modified', last_mod)
514 521 self.finish(data)
515 522
516 523 @web.authenticated
517 524 def put(self, notebook_id):
518 525 nbm = self.application.notebook_manager
519 526 format = self.get_argument('format', default='json')
520 527 name = self.get_argument('name', default=None)
521 528 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
522 529 self.set_status(204)
523 530 self.finish()
524 531
525 532 @web.authenticated
526 533 def delete(self, notebook_id):
527 534 nbm = self.application.notebook_manager
528 535 nbm.delete_notebook(notebook_id)
529 536 self.set_status(204)
530 537 self.finish()
531 538
532 539 #-----------------------------------------------------------------------------
533 540 # RST web service handlers
534 541 #-----------------------------------------------------------------------------
535 542
536 543
537 544 class RSTHandler(AuthenticatedHandler):
538 545
539 546 @web.authenticated
540 547 def post(self):
541 548 if publish_string is None:
542 549 raise web.HTTPError(503, u'docutils not available')
543 550 body = self.request.body.strip()
544 551 source = body
545 552 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
546 553 defaults = {'file_insertion_enabled': 0,
547 554 'raw_enabled': 0,
548 555 '_disable_config': 1,
549 556 'stylesheet_path': 0
550 557 # 'template': template_path
551 558 }
552 559 try:
553 560 html = publish_string(source, writer_name='html',
554 561 settings_overrides=defaults
555 562 )
556 563 except:
557 564 raise web.HTTPError(400, u'Invalid RST')
558 565 print html
559 566 self.set_header('Content-Type', 'text/html')
560 567 self.finish(html)
561 568
562 569
@@ -1,61 +1,8 b''
1 <!DOCTYPE HTML>
2 <html>
3
4 <head>
5 <meta charset="utf-8">
6
7 <title>IPython Notebook</title>
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
13
14 <meta name="read_only" content="{{read_only}}"/>
15
16 </head>
17
18 <body>
19
20 <div id="header">
21 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
22 </div>
23
24 <div id="header_border"></div>
25
26 <div id="main_app">
27
28 <div id="app_hbox">
29
30 <div id="left_panel">
31 </div>
32
33 <div id="content_panel">
34 {% if message %}
35 <div id="message">
36 {{message}}
37 </div>
38 {% end %}
1 {% extends layout.html %}
39 2
3 {% block content_panel %}
40 4 <form action="/login?next={{url_escape(next)}}" method="post">
41 5 Password: <input type="password" name="password">
42 6 <input type="submit" value="Sign in" id="signin">
43 7 </form>
44 </div>
45 <div id="right_panel">
46 </div>
47
48 </div>
49
50 </div>
51
52 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
53 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
54 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
55 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
56
57 </body>
58
59 </html>
60
61
8 {% end %}
@@ -1,69 +1,26 b''
1 <!DOCTYPE HTML>
2 <html>
1 {% extends layout.html %}
3 2
4 <head>
5 <meta charset="utf-8">
3 {% block title %}
4 IPython Dashboard
5 {% end %}
6 6
7 <title>IPython Dashboard</title>
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css" />
7 {% block stylesheet %}
13 8 <link rel="stylesheet" href="static/css/projectdashboard.css" type="text/css" />
9 {% end %}
14 10
15 <meta name="read_only" content="{{read_only}}"/>
16
17 </head>
18
19 <body data-project={{project}} data-base-project-url={{base_project_url}}
20 data-base-kernel-url={{base_kernel_url}}>
21
22 <div id="header">
23 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
24 <span id="login_widget" class="hidden">
25 <button id="login">Login</button>
26 </span>
27 </div>
28
29 <div id="header_border"></div>
30
31 <div id="main_app">
32
33 <div id="app_hbox">
34
35 <div id="left_panel">
36 </div>
37
38 <div id="content_panel">
11 {% block content_panel %}
39 12 <div id="content_toolbar">
40 13 <span id="drag_info">Drag files onto the list to import notebooks.</span>
41 14 <span id="notebooks_buttons">
42 15 <button id="new_notebook">New Notebook</button>
43 16 </span>
44 17 </div>
45 18 <div id="notebook_list">
46 19 <div id="project_name"><h2>{{project}}</h2></div>
47 20 </div>
21 {% end %}
48 22
49 </div>
50
51 <div id="right_panel">
52 </div>
53
54 </div>
55
56 </div>
57
58 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
59 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
60 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
23 {% block script %}
61 24 <script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
62 <script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
63 25 <script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
64
65 </body>
66
67 </html>
68
69
26 {% end %}
General Comments 0
You need to be logged in to leave comments. Login now