##// END OF EJS Templates
Refactoring templates and top level js/css organization.
Brian Granger -
Show More
@@ -0,0 +1,6
1
2 #main_app {
3 height: 100px;
4 width: 350px;
5 margin: 50px auto;
6 }
@@ -0,0 +1,7
1
2 #main_app {
3 height: 100px;
4 width: 200px;
5 margin: 50px auto;
6 }
7
@@ -0,0 +1,20
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
7
8 //============================================================================
9 // On document ready
10 //============================================================================
11
12
13 $(document).ready(function () {
14
15 IPython.page = new IPython.Page();
16 $('div#main_app').addClass('border-box-sizing ui-widget');
17 IPython.page.show();
18
19 });
20
@@ -0,0 +1,44
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
7
8 //============================================================================
9 // Global header/site setup.
10 //============================================================================
11
12 var IPython = (function (IPython) {
13
14 var Page = function () {
15 this.style();
16 this.bind_events();
17 };
18
19 Page.prototype.style = function () {
20 $('div#header').addClass('border-box-sizing').
21 addClass('ui-widget ui-widget-content').
22 css('border-top-style','none').
23 css('border-left-style','none').
24 css('border-right-style','none');
25 $('div#site').addClass('border-box-sizing')
26 };
27
28
29 Page.prototype.bind_events = function () {
30 };
31
32
33 Page.prototype.show = function () {
34 // The header and site divs start out hidden to prevent FLOUC.
35 // Main scripts should call this method after styling everything.
36 $('div#header').css('display','block');
37 $('div#site').css('display','block');
38 };
39
40 IPython.Page = Page;
41
42 return IPython;
43
44 }(IPython));
@@ -0,0 +1,19
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
7
8 //============================================================================
9 // On document ready
10 //============================================================================
11
12
13 $(document).ready(function () {
14
15 IPython.page = new IPython.Page();
16 IPython.page.show();
17
18 });
19
@@ -1,728 +1,730
1 1 """Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import logging
20 20 import Cookie
21 21 import time
22 22 import uuid
23 23
24 24 from tornado import web
25 25 from tornado import websocket
26 26
27 27 from zmq.eventloop import ioloop
28 28 from zmq.utils import jsonapi
29 29
30 30 from IPython.external.decorator import decorator
31 31 from IPython.zmq.session import Session
32 32 from IPython.lib.security import passwd_check
33 33
34 34 try:
35 35 from docutils.core import publish_string
36 36 except ImportError:
37 37 publish_string = None
38 38
39 39 #-----------------------------------------------------------------------------
40 40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
41 41 #-----------------------------------------------------------------------------
42 42
43 43 # Google Chrome, as of release 16, changed its websocket protocol number. The
44 44 # parts tornado cares about haven't really changed, so it's OK to continue
45 45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
46 46 # version as of Oct 30/2011) the version check fails, see the issue report:
47 47
48 48 # https://github.com/facebook/tornado/issues/385
49 49
50 50 # This issue has been fixed in Tornado post 2.1.1:
51 51
52 52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
53 53
54 54 # Here we manually apply the same patch as above so that users of IPython can
55 55 # continue to work with an officially released Tornado. We make the
56 56 # monkeypatch version check as narrow as possible to limit its effects; once
57 57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
58 58
59 59 import tornado
60 60
61 61 if tornado.version_info <= (2,1,1):
62 62
63 63 def _execute(self, transforms, *args, **kwargs):
64 64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
65 65
66 66 self.open_args = args
67 67 self.open_kwargs = kwargs
68 68
69 69 # The difference between version 8 and 13 is that in 8 the
70 70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
71 71 # simply "Origin".
72 72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
73 73 self.ws_connection = WebSocketProtocol8(self)
74 74 self.ws_connection.accept_connection()
75 75
76 76 elif self.request.headers.get("Sec-WebSocket-Version"):
77 77 self.stream.write(tornado.escape.utf8(
78 78 "HTTP/1.1 426 Upgrade Required\r\n"
79 79 "Sec-WebSocket-Version: 8\r\n\r\n"))
80 80 self.stream.close()
81 81
82 82 else:
83 83 self.ws_connection = WebSocketProtocol76(self)
84 84 self.ws_connection.accept_connection()
85 85
86 86 websocket.WebSocketHandler._execute = _execute
87 87 del _execute
88 88
89 89 #-----------------------------------------------------------------------------
90 90 # Decorator for disabling read-only handlers
91 91 #-----------------------------------------------------------------------------
92 92
93 93 @decorator
94 94 def not_if_readonly(f, self, *args, **kwargs):
95 95 if self.application.read_only:
96 96 raise web.HTTPError(403, "Notebook server is read-only")
97 97 else:
98 98 return f(self, *args, **kwargs)
99 99
100 100 @decorator
101 101 def authenticate_unless_readonly(f, self, *args, **kwargs):
102 102 """authenticate this page *unless* readonly view is active.
103 103
104 104 In read-only mode, the notebook list and print view should
105 105 be accessible without authentication.
106 106 """
107 107
108 108 @web.authenticated
109 109 def auth_f(self, *args, **kwargs):
110 110 return f(self, *args, **kwargs)
111 111
112 112 if self.application.read_only:
113 113 return f(self, *args, **kwargs)
114 114 else:
115 115 return auth_f(self, *args, **kwargs)
116 116
117 117 #-----------------------------------------------------------------------------
118 118 # Top-level handlers
119 119 #-----------------------------------------------------------------------------
120 120
121 121 class RequestHandler(web.RequestHandler):
122 122 """RequestHandler with default variable setting."""
123 123
124 124 def render(*args, **kwargs):
125 125 kwargs.setdefault('message', '')
126 126 return web.RequestHandler.render(*args, **kwargs)
127 127
128 128 class AuthenticatedHandler(RequestHandler):
129 129 """A RequestHandler with an authenticated user."""
130 130
131 131 def get_current_user(self):
132 132 user_id = self.get_secure_cookie("username")
133 133 # For now the user_id should not return empty, but it could eventually
134 134 if user_id == '':
135 135 user_id = 'anonymous'
136 136 if user_id is None:
137 137 # prevent extra Invalid cookie sig warnings:
138 138 self.clear_cookie('username')
139 139 if not self.application.password and not self.application.read_only:
140 140 user_id = 'anonymous'
141 141 return user_id
142 142
143 143 @property
144 144 def logged_in(self):
145 145 """Is a user currently logged in?
146 146
147 147 """
148 148 user = self.get_current_user()
149 149 return (user and not user == 'anonymous')
150 150
151 151 @property
152 152 def login_available(self):
153 153 """May a user proceed to log in?
154 154
155 155 This returns True if login capability is available, irrespective of
156 156 whether the user is already logged in or not.
157 157
158 158 """
159 159 return bool(self.application.password)
160 160
161 161 @property
162 162 def read_only(self):
163 163 """Is the notebook read-only?
164 164
165 165 """
166 166 return self.application.read_only
167 167
168 168 @property
169 169 def ws_url(self):
170 170 """websocket url matching the current request
171 171
172 172 turns http[s]://host[:port] into
173 173 ws[s]://host[:port]
174 174 """
175 175 proto = self.request.protocol.replace('http', 'ws')
176 176 host = self.application.ipython_app.websocket_host # default to config value
177 177 if host == '':
178 178 host = self.request.host # get from request
179 179 return "%s://%s" % (proto, host)
180 180
181 181
182 182 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
183 183 """static files should only be accessible when logged in"""
184 184
185 185 @authenticate_unless_readonly
186 186 def get(self, path):
187 187 return web.StaticFileHandler.get(self, path)
188 188
189 189
190 190 class ProjectDashboardHandler(AuthenticatedHandler):
191 191
192 192 @authenticate_unless_readonly
193 193 def get(self):
194 194 nbm = self.application.notebook_manager
195 195 project = nbm.notebook_dir
196 196 self.render(
197 197 'projectdashboard.html', project=project,
198 198 base_project_url=self.application.ipython_app.base_project_url,
199 199 base_kernel_url=self.application.ipython_app.base_kernel_url,
200 200 read_only=self.read_only,
201 201 logged_in=self.logged_in,
202 202 login_available=self.login_available
203 203 )
204 204
205 205
206 206 class LoginHandler(AuthenticatedHandler):
207 207
208 208 def _render(self, message=None):
209 209 self.render('login.html',
210 210 next=self.get_argument('next', default='/'),
211 211 read_only=self.read_only,
212 212 logged_in=self.logged_in,
213 213 login_available=self.login_available,
214 base_project_url=self.application.ipython_app.base_project_url,
214 215 message=message
215 216 )
216 217
217 218 def get(self):
218 219 if self.current_user:
219 220 self.redirect(self.get_argument('next', default='/'))
220 221 else:
221 222 self._render()
222 223
223 224 def post(self):
224 225 pwd = self.get_argument('password', default=u'')
225 226 if self.application.password:
226 227 if passwd_check(self.application.password, pwd):
227 228 self.set_secure_cookie('username', str(uuid.uuid4()))
228 229 else:
229 230 self._render(message={'error': 'Invalid password'})
230 231 return
231 232
232 233 self.redirect(self.get_argument('next', default='/'))
233 234
234 235
235 236 class LogoutHandler(AuthenticatedHandler):
236 237
237 238 def get(self):
238 239 self.clear_cookie('username')
239 240 if self.login_available:
240 241 message = {'info': 'Successfully logged out.'}
241 242 else:
242 243 message = {'warning': 'Cannot log out. Notebook authentication '
243 244 'is disabled.'}
244 245
245 246 self.render('logout.html',
246 247 read_only=self.read_only,
247 248 logged_in=self.logged_in,
248 249 login_available=self.login_available,
250 base_project_url=self.application.ipython_app.base_project_url,
249 251 message=message)
250 252
251 253
252 254 class NewHandler(AuthenticatedHandler):
253 255
254 256 @web.authenticated
255 257 def get(self):
256 258 nbm = self.application.notebook_manager
257 259 project = nbm.notebook_dir
258 260 notebook_id = nbm.new_notebook()
259 261 self.render(
260 262 'notebook.html', project=project,
261 263 notebook_id=notebook_id,
262 264 base_project_url=self.application.ipython_app.base_project_url,
263 265 base_kernel_url=self.application.ipython_app.base_kernel_url,
264 266 kill_kernel=False,
265 267 read_only=False,
266 268 logged_in=self.logged_in,
267 269 login_available=self.login_available,
268 270 mathjax_url=self.application.ipython_app.mathjax_url,
269 271 )
270 272
271 273
272 274 class NamedNotebookHandler(AuthenticatedHandler):
273 275
274 276 @authenticate_unless_readonly
275 277 def get(self, notebook_id):
276 278 nbm = self.application.notebook_manager
277 279 project = nbm.notebook_dir
278 280 if not nbm.notebook_exists(notebook_id):
279 281 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
280 282
281 283 self.render(
282 284 'notebook.html', project=project,
283 285 notebook_id=notebook_id,
284 286 base_project_url=self.application.ipython_app.base_project_url,
285 287 base_kernel_url=self.application.ipython_app.base_kernel_url,
286 288 kill_kernel=False,
287 289 read_only=self.read_only,
288 290 logged_in=self.logged_in,
289 291 login_available=self.login_available,
290 292 mathjax_url=self.application.ipython_app.mathjax_url,
291 293 )
292 294
293 295
294 296 class PrintNotebookHandler(AuthenticatedHandler):
295 297
296 298 @authenticate_unless_readonly
297 299 def get(self, notebook_id):
298 300 nbm = self.application.notebook_manager
299 301 project = nbm.notebook_dir
300 302 if not nbm.notebook_exists(notebook_id):
301 303 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
302 304
303 305 self.render(
304 306 'printnotebook.html', project=project,
305 307 notebook_id=notebook_id,
306 308 base_project_url=self.application.ipython_app.base_project_url,
307 309 base_kernel_url=self.application.ipython_app.base_kernel_url,
308 310 kill_kernel=False,
309 311 read_only=self.read_only,
310 312 logged_in=self.logged_in,
311 313 login_available=self.login_available,
312 314 mathjax_url=self.application.ipython_app.mathjax_url,
313 315 )
314 316
315 317 #-----------------------------------------------------------------------------
316 318 # Kernel handlers
317 319 #-----------------------------------------------------------------------------
318 320
319 321
320 322 class MainKernelHandler(AuthenticatedHandler):
321 323
322 324 @web.authenticated
323 325 def get(self):
324 326 km = self.application.kernel_manager
325 327 self.finish(jsonapi.dumps(km.kernel_ids))
326 328
327 329 @web.authenticated
328 330 def post(self):
329 331 km = self.application.kernel_manager
330 332 notebook_id = self.get_argument('notebook', default=None)
331 333 kernel_id = km.start_kernel(notebook_id)
332 334 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
333 335 self.set_header('Location', '/'+kernel_id)
334 336 self.finish(jsonapi.dumps(data))
335 337
336 338
337 339 class KernelHandler(AuthenticatedHandler):
338 340
339 341 SUPPORTED_METHODS = ('DELETE')
340 342
341 343 @web.authenticated
342 344 def delete(self, kernel_id):
343 345 km = self.application.kernel_manager
344 346 km.kill_kernel(kernel_id)
345 347 self.set_status(204)
346 348 self.finish()
347 349
348 350
349 351 class KernelActionHandler(AuthenticatedHandler):
350 352
351 353 @web.authenticated
352 354 def post(self, kernel_id, action):
353 355 km = self.application.kernel_manager
354 356 if action == 'interrupt':
355 357 km.interrupt_kernel(kernel_id)
356 358 self.set_status(204)
357 359 if action == 'restart':
358 360 new_kernel_id = km.restart_kernel(kernel_id)
359 361 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
360 362 self.set_header('Location', '/'+new_kernel_id)
361 363 self.write(jsonapi.dumps(data))
362 364 self.finish()
363 365
364 366
365 367 class ZMQStreamHandler(websocket.WebSocketHandler):
366 368
367 369 def _reserialize_reply(self, msg_list):
368 370 """Reserialize a reply message using JSON.
369 371
370 372 This takes the msg list from the ZMQ socket, unserializes it using
371 373 self.session and then serializes the result using JSON. This method
372 374 should be used by self._on_zmq_reply to build messages that can
373 375 be sent back to the browser.
374 376 """
375 377 idents, msg_list = self.session.feed_identities(msg_list)
376 378 msg = self.session.unserialize(msg_list)
377 379 try:
378 380 msg['header'].pop('date')
379 381 except KeyError:
380 382 pass
381 383 try:
382 384 msg['parent_header'].pop('date')
383 385 except KeyError:
384 386 pass
385 387 msg.pop('buffers')
386 388 return jsonapi.dumps(msg)
387 389
388 390 def _on_zmq_reply(self, msg_list):
389 391 try:
390 392 msg = self._reserialize_reply(msg_list)
391 393 except:
392 394 self.application.log.critical("Malformed message: %r" % msg_list)
393 395 else:
394 396 self.write_message(msg)
395 397
396 398 def allow_draft76(self):
397 399 """Allow draft 76, until browsers such as Safari update to RFC 6455.
398 400
399 401 This has been disabled by default in tornado in release 2.2.0, and
400 402 support will be removed in later versions.
401 403 """
402 404 return True
403 405
404 406
405 407 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
406 408
407 409 def open(self, kernel_id):
408 410 self.kernel_id = kernel_id.decode('ascii')
409 411 try:
410 412 cfg = self.application.ipython_app.config
411 413 except AttributeError:
412 414 # protect from the case where this is run from something other than
413 415 # the notebook app:
414 416 cfg = None
415 417 self.session = Session(config=cfg)
416 418 self.save_on_message = self.on_message
417 419 self.on_message = self.on_first_message
418 420
419 421 def get_current_user(self):
420 422 user_id = self.get_secure_cookie("username")
421 423 if user_id == '' or (user_id is None and not self.application.password):
422 424 user_id = 'anonymous'
423 425 return user_id
424 426
425 427 def _inject_cookie_message(self, msg):
426 428 """Inject the first message, which is the document cookie,
427 429 for authentication."""
428 430 if isinstance(msg, unicode):
429 431 # Cookie can't constructor doesn't accept unicode strings for some reason
430 432 msg = msg.encode('utf8', 'replace')
431 433 try:
432 434 self.request._cookies = Cookie.SimpleCookie(msg)
433 435 except:
434 436 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
435 437
436 438 def on_first_message(self, msg):
437 439 self._inject_cookie_message(msg)
438 440 if self.get_current_user() is None:
439 441 logging.warn("Couldn't authenticate WebSocket connection")
440 442 raise web.HTTPError(403)
441 443 self.on_message = self.save_on_message
442 444
443 445
444 446 class IOPubHandler(AuthenticatedZMQStreamHandler):
445 447
446 448 def initialize(self, *args, **kwargs):
447 449 self._kernel_alive = True
448 450 self._beating = False
449 451 self.iopub_stream = None
450 452 self.hb_stream = None
451 453
452 454 def on_first_message(self, msg):
453 455 try:
454 456 super(IOPubHandler, self).on_first_message(msg)
455 457 except web.HTTPError:
456 458 self.close()
457 459 return
458 460 km = self.application.kernel_manager
459 461 self.time_to_dead = km.time_to_dead
460 462 self.first_beat = km.first_beat
461 463 kernel_id = self.kernel_id
462 464 try:
463 465 self.iopub_stream = km.create_iopub_stream(kernel_id)
464 466 self.hb_stream = km.create_hb_stream(kernel_id)
465 467 except web.HTTPError:
466 468 # WebSockets don't response to traditional error codes so we
467 469 # close the connection.
468 470 if not self.stream.closed():
469 471 self.stream.close()
470 472 self.close()
471 473 else:
472 474 self.iopub_stream.on_recv(self._on_zmq_reply)
473 475 self.start_hb(self.kernel_died)
474 476
475 477 def on_message(self, msg):
476 478 pass
477 479
478 480 def on_close(self):
479 481 # This method can be called twice, once by self.kernel_died and once
480 482 # from the WebSocket close event. If the WebSocket connection is
481 483 # closed before the ZMQ streams are setup, they could be None.
482 484 self.stop_hb()
483 485 if self.iopub_stream is not None and not self.iopub_stream.closed():
484 486 self.iopub_stream.on_recv(None)
485 487 self.iopub_stream.close()
486 488 if self.hb_stream is not None and not self.hb_stream.closed():
487 489 self.hb_stream.close()
488 490
489 491 def start_hb(self, callback):
490 492 """Start the heartbeating and call the callback if the kernel dies."""
491 493 if not self._beating:
492 494 self._kernel_alive = True
493 495
494 496 def ping_or_dead():
495 497 self.hb_stream.flush()
496 498 if self._kernel_alive:
497 499 self._kernel_alive = False
498 500 self.hb_stream.send(b'ping')
499 501 # flush stream to force immediate socket send
500 502 self.hb_stream.flush()
501 503 else:
502 504 try:
503 505 callback()
504 506 except:
505 507 pass
506 508 finally:
507 509 self.stop_hb()
508 510
509 511 def beat_received(msg):
510 512 self._kernel_alive = True
511 513
512 514 self.hb_stream.on_recv(beat_received)
513 515 loop = ioloop.IOLoop.instance()
514 516 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
515 517 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
516 518 self._beating= True
517 519
518 520 def _really_start_hb(self):
519 521 """callback for delayed heartbeat start
520 522
521 523 Only start the hb loop if we haven't been closed during the wait.
522 524 """
523 525 if self._beating and not self.hb_stream.closed():
524 526 self._hb_periodic_callback.start()
525 527
526 528 def stop_hb(self):
527 529 """Stop the heartbeating and cancel all related callbacks."""
528 530 if self._beating:
529 531 self._beating = False
530 532 self._hb_periodic_callback.stop()
531 533 if not self.hb_stream.closed():
532 534 self.hb_stream.on_recv(None)
533 535
534 536 def kernel_died(self):
535 537 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
536 538 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
537 539 self.write_message(
538 540 {'header': {'msg_type': 'status'},
539 541 'parent_header': {},
540 542 'content': {'execution_state':'dead'}
541 543 }
542 544 )
543 545 self.on_close()
544 546
545 547
546 548 class ShellHandler(AuthenticatedZMQStreamHandler):
547 549
548 550 def initialize(self, *args, **kwargs):
549 551 self.shell_stream = None
550 552
551 553 def on_first_message(self, msg):
552 554 try:
553 555 super(ShellHandler, self).on_first_message(msg)
554 556 except web.HTTPError:
555 557 self.close()
556 558 return
557 559 km = self.application.kernel_manager
558 560 self.max_msg_size = km.max_msg_size
559 561 kernel_id = self.kernel_id
560 562 try:
561 563 self.shell_stream = km.create_shell_stream(kernel_id)
562 564 except web.HTTPError:
563 565 # WebSockets don't response to traditional error codes so we
564 566 # close the connection.
565 567 if not self.stream.closed():
566 568 self.stream.close()
567 569 self.close()
568 570 else:
569 571 self.shell_stream.on_recv(self._on_zmq_reply)
570 572
571 573 def on_message(self, msg):
572 574 if len(msg) < self.max_msg_size:
573 575 msg = jsonapi.loads(msg)
574 576 self.session.send(self.shell_stream, msg)
575 577
576 578 def on_close(self):
577 579 # Make sure the stream exists and is not already closed.
578 580 if self.shell_stream is not None and not self.shell_stream.closed():
579 581 self.shell_stream.close()
580 582
581 583
582 584 #-----------------------------------------------------------------------------
583 585 # Notebook web service handlers
584 586 #-----------------------------------------------------------------------------
585 587
586 588 class NotebookRootHandler(AuthenticatedHandler):
587 589
588 590 @authenticate_unless_readonly
589 591 def get(self):
590 592 nbm = self.application.notebook_manager
591 593 files = nbm.list_notebooks()
592 594 self.finish(jsonapi.dumps(files))
593 595
594 596 @web.authenticated
595 597 def post(self):
596 598 nbm = self.application.notebook_manager
597 599 body = self.request.body.strip()
598 600 format = self.get_argument('format', default='json')
599 601 name = self.get_argument('name', default=None)
600 602 if body:
601 603 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
602 604 else:
603 605 notebook_id = nbm.new_notebook()
604 606 self.set_header('Location', '/'+notebook_id)
605 607 self.finish(jsonapi.dumps(notebook_id))
606 608
607 609
608 610 class NotebookHandler(AuthenticatedHandler):
609 611
610 612 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
611 613
612 614 @authenticate_unless_readonly
613 615 def get(self, notebook_id):
614 616 nbm = self.application.notebook_manager
615 617 format = self.get_argument('format', default='json')
616 618 last_mod, name, data = nbm.get_notebook(notebook_id, format)
617 619
618 620 if format == u'json':
619 621 self.set_header('Content-Type', 'application/json')
620 622 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
621 623 elif format == u'py':
622 624 self.set_header('Content-Type', 'application/x-python')
623 625 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
624 626 self.set_header('Last-Modified', last_mod)
625 627 self.finish(data)
626 628
627 629 @web.authenticated
628 630 def put(self, notebook_id):
629 631 nbm = self.application.notebook_manager
630 632 format = self.get_argument('format', default='json')
631 633 name = self.get_argument('name', default=None)
632 634 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
633 635 self.set_status(204)
634 636 self.finish()
635 637
636 638 @web.authenticated
637 639 def delete(self, notebook_id):
638 640 nbm = self.application.notebook_manager
639 641 nbm.delete_notebook(notebook_id)
640 642 self.set_status(204)
641 643 self.finish()
642 644
643 645
644 646 class NotebookCopyHandler(AuthenticatedHandler):
645 647
646 648 @web.authenticated
647 649 def get(self, notebook_id):
648 650 nbm = self.application.notebook_manager
649 651 project = nbm.notebook_dir
650 652 notebook_id = nbm.copy_notebook(notebook_id)
651 653 self.render(
652 654 'notebook.html', project=project,
653 655 notebook_id=notebook_id,
654 656 base_project_url=self.application.ipython_app.base_project_url,
655 657 base_kernel_url=self.application.ipython_app.base_kernel_url,
656 658 kill_kernel=False,
657 659 read_only=False,
658 660 logged_in=self.logged_in,
659 661 login_available=self.login_available,
660 662 mathjax_url=self.application.ipython_app.mathjax_url,
661 663 )
662 664
663 665
664 666 #-----------------------------------------------------------------------------
665 667 # Cluster handlers
666 668 #-----------------------------------------------------------------------------
667 669
668 670
669 671 class MainClusterHandler(AuthenticatedHandler):
670 672
671 673 @web.authenticated
672 674 def get(self):
673 675 cm = self.application.cluster_manager
674 676 self.finish(jsonapi.dumps(cm.list_profiles()))
675 677
676 678
677 679 class ClusterProfileHandler(AuthenticatedHandler):
678 680
679 681 @web.authenticated
680 682 def get(self, profile):
681 683 cm = self.application.cluster_manager
682 684 self.finish(jsonapi.dumps(cm.profile_info(profile)))
683 685
684 686
685 687 class ClusterActionHandler(AuthenticatedHandler):
686 688
687 689 @web.authenticated
688 690 def post(self, profile, action):
689 691 cm = self.application.cluster_manager
690 692 if action == 'start':
691 693 n = int(self.get_argument('n', default=4))
692 694 cm.start_cluster(profile, n)
693 695 if action == 'stop':
694 696 cm.stop_cluster(profile)
695 697 self.finish()
696 698
697 699
698 700 #-----------------------------------------------------------------------------
699 701 # RST web service handlers
700 702 #-----------------------------------------------------------------------------
701 703
702 704
703 705 class RSTHandler(AuthenticatedHandler):
704 706
705 707 @web.authenticated
706 708 def post(self):
707 709 if publish_string is None:
708 710 raise web.HTTPError(503, u'docutils not available')
709 711 body = self.request.body.strip()
710 712 source = body
711 713 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
712 714 defaults = {'file_insertion_enabled': 0,
713 715 'raw_enabled': 0,
714 716 '_disable_config': 1,
715 717 'stylesheet_path': 0
716 718 # 'template': template_path
717 719 }
718 720 try:
719 721 html = publish_string(source, writer_name='html',
720 722 settings_overrides=defaults
721 723 )
722 724 except:
723 725 raise web.HTTPError(400, u'Invalid RST')
724 726 print html
725 727 self.set_header('Content-Type', 'text/html')
726 728 self.finish(html)
727 729
728 730
@@ -1,130 +1,97
1 1
2 .border-box-sizing {
3 box-sizing: border-box;
4 -moz-box-sizing: border-box;
5 -webkit-box-sizing: border-box;
6 }
7
8 2 /* Flexible box model classes */
9 3 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
10 4
11 5 .hbox {
12 6 display: -webkit-box;
13 7 -webkit-box-orient: horizontal;
14 8 -webkit-box-align: stretch;
15 9
16 10 display: -moz-box;
17 11 -moz-box-orient: horizontal;
18 12 -moz-box-align: stretch;
19 13
20 14 display: box;
21 15 box-orient: horizontal;
22 16 box-align: stretch;
23 17 }
24 18
25 19 .hbox > * {
26 20 -webkit-box-flex: 0;
27 21 -moz-box-flex: 0;
28 22 box-flex: 0;
29 23 }
30 24
31 25 .vbox {
32 26 display: -webkit-box;
33 27 -webkit-box-orient: vertical;
34 28 -webkit-box-align: stretch;
35 29
36 30 display: -moz-box;
37 31 -moz-box-orient: vertical;
38 32 -moz-box-align: stretch;
39 33
40 34 display: box;
41 35 box-orient: vertical;
42 36 box-align: stretch;
43 37 }
44 38
45 39 .vbox > * {
46 40 -webkit-box-flex: 0;
47 41 -moz-box-flex: 0;
48 42 box-flex: 0;
49 43 }
50 44
51 45 .reverse {
52 46 -webkit-box-direction: reverse;
53 47 -moz-box-direction: reverse;
54 48 box-direction: reverse;
55 49 }
56 50
57 51 .box-flex0 {
58 52 -webkit-box-flex: 0;
59 53 -moz-box-flex: 0;
60 54 box-flex: 0;
61 55 }
62 56
63 57 .box-flex1, .box-flex {
64 58 -webkit-box-flex: 1;
65 59 -moz-box-flex: 1;
66 60 box-flex: 1;
67 61 }
68 62
69 63 .box-flex2 {
70 64 -webkit-box-flex: 2;
71 65 -moz-box-flex: 2;
72 66 box-flex: 2;
73 67 }
74 68
75 69 .box-group1 {
76 70 -webkit-box-flex-group: 1;
77 71 -moz-box-flex-group: 1;
78 72 box-flex-group: 1;
79 73 }
80 74
81 75 .box-group2 {
82 76 -webkit-box-flex-group: 2;
83 77 -moz-box-flex-group: 2;
84 78 box-flex-group: 2;
85 79 }
86 80
87 81 .start {
88 82 -webkit-box-pack: start;
89 83 -moz-box-pack: start;
90 84 box-pack: start;
91 85 }
92 86
93 87 .end {
94 88 -webkit-box-pack: end;
95 89 -moz-box-pack: end;
96 90 box-pack: end;
97 91 }
98 92
99 93 .center {
100 94 -webkit-box-pack: center;
101 95 -moz-box-pack: center;
102 96 box-pack: center;
103 97 }
104
105 .message {
106 border-width: 1px;
107 border-style: solid;
108 text-align: center;
109 padding: 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;
126 }
127
128 #content_panel {
129 margin: 0.5em;
130 } No newline at end of file
@@ -1,381 +1,389
1 1 /**
2 2 * Primary styles
3 3 *
4 4 * Author: IPython Development Team
5 5 */
6 6
7 7
8 8 body {
9 9 background-color: white;
10 10 /* This makes sure that the body covers the entire window and needs to
11 11 be in a different element than the display: box in wrapper below */
12 12 position: absolute;
13 13 left: 0px;
14 14 right: 0px;
15 15 top: 0px;
16 16 bottom: 0px;
17 17 overflow: hidden;
18 18 }
19 19
20 20 span#save_widget {
21 21 padding: 5px;
22 22 margin: 0px 0px 0px 300px;
23 23 display:inline-block;
24 24 }
25 25
26 26 span#notebook_name {
27 27 height: 1em;
28 28 line-height: 1em;
29 29 padding: 3px;
30 30 border: none;
31 31 font-size: 146.5%;
32 32 }
33 33
34 34 #menubar {
35 35 /* Initially hidden to prevent FLOUC */
36 36 display: none;
37 37 }
38 38
39 39 .ui-menubar-item .ui-button .ui-button-text {
40 40 padding: 0.4em 1.0em;
41 41 font-size: 100%;
42 42 }
43 43
44 44 .ui-menu {
45 45 -moz-box-shadow: 0px 6px 10px -1px #adadad;
46 46 -webkit-box-shadow: 0px 6px 10px -1px #adadad;
47 47 box-shadow: 0px 6px 10px -1px #adadad;
48 48 }
49 49
50 50 .ui-menu .ui-menu-item a {
51 51 padding: 2px 1.6em;
52 52 }
53 53
54 54 .ui-menu hr {
55 55 margin: 0.3em 0;
56 56 }
57 57
58 58 #menubar_container {
59 59 position: relative;
60 60 }
61 61
62 62 #notification {
63 63 position: absolute;
64 64 right: 3px;
65 65 top: 3px;
66 66 height: 25px;
67 67 padding: 3px 6px;
68 68 z-index: 10;
69 69 }
70 70
71 71 #toolbar {
72 72 /* Initially hidden to prevent FLOUC */
73 73 display: none;
74 74 padding: 3px 15px;
75 75 }
76 76
77 77 #cell_type {
78 78 font-size: 85%;
79 79 }
80 80
81
82 div#main_app {
83 /* Initially hidden to prevent FLOUC */
84 display: none;
85 width: 100%;
86 position: relative;
87 }
88
81 89 span#quick_help_area {
82 90 position: static;
83 91 padding: 5px 0px;
84 92 margin: 0px 0px 0px 0px;
85 93 }
86 94
87 95 .help_string {
88 96 float: right;
89 97 width: 170px;
90 98 padding: 0px 5px;
91 99 text-align: left;
92 100 font-size: 85%;
93 101 }
94 102
95 103 .help_string_label {
96 104 float: right;
97 105 font-size: 85%;
98 106 }
99 107
100 108 div#notebook_panel {
101 109 margin: 0px 0px 0px 0px;
102 110 padding: 0px;
103 111 }
104 112
105 113 div#notebook {
106 114 overflow-y: scroll;
107 115 overflow-x: auto;
108 116 width: 100%;
109 117 /* This spaces the cell away from the edge of the notebook area */
110 118 padding: 5px 5px 15px 5px;
111 119 margin: 0px
112 120 background-color: white;
113 121 }
114 122
115 123 div#pager_splitter {
116 124 height: 8px;
117 125 }
118 126
119 127 div#pager {
120 128 padding: 15px;
121 129 overflow: auto;
122 130 display: none;
123 131 }
124 132
125 133 div.cell {
126 134 width: 100%;
127 135 padding: 5px 5px 5px 0px;
128 136 /* This acts as a spacer between cells, that is outside the border */
129 137 margin: 2px 0px 2px 0px;
130 138 }
131 139
132 140 div.code_cell {
133 141 background-color: white;
134 142 }
135 143 /* any special styling for code cells that are currently running goes here */
136 144 div.code_cell.running {
137 145 }
138 146
139 147 div.prompt {
140 148 /* This needs to be wide enough for 3 digit prompt numbers: In[100]: */
141 149 width: 11ex;
142 150 /* This 0.4em is tuned to match the padding on the CodeMirror editor. */
143 151 padding: 0.4em;
144 152 margin: 0px;
145 153 font-family: monospace;
146 154 text-align:right;
147 155 }
148 156
149 157 div.input {
150 158 page-break-inside: avoid;
151 159 }
152 160
153 161 /* input_area and input_prompt must match in top border and margin for alignment */
154 162 div.input_area {
155 163 color: black;
156 164 border: 1px solid #ddd;
157 165 border-radius: 3px;
158 166 background: #f7f7f7;
159 167 }
160 168
161 169 div.input_prompt {
162 170 color: navy;
163 171 border-top: 1px solid transparent;
164 172 }
165 173
166 174 div.output {
167 175 /* This is a spacer between the input and output of each cell */
168 176 margin-top: 5px;
169 177 }
170 178
171 179 div.output_prompt {
172 180 color: darkred;
173 181 }
174 182
175 183 /* This class is the outer container of all output sections. */
176 184 div.output_area {
177 185 padding: 0px;
178 186 page-break-inside: avoid;
179 187 }
180 188
181 189 /* This class is for the output subarea inside the output_area and after
182 190 the prompt div. */
183 191 div.output_subarea {
184 192 padding: 0.4em 6.1em 0.4em 0.4em;
185 193 }
186 194
187 195 /* The rest of the output_* classes are for special styling of the different
188 196 output types */
189 197
190 198 /* all text output has this class: */
191 199 div.output_text {
192 200 text-align: left;
193 201 color: black;
194 202 font-family: monospace;
195 203 }
196 204
197 205 /* stdout/stderr are 'text' as well as 'stream', but pyout/pyerr are *not* streams */
198 206 div.output_stream {
199 207 padding-top: 0.0em;
200 208 padding-bottom: 0.0em;
201 209 }
202 210 div.output_stdout {
203 211 }
204 212 div.output_stderr {
205 213 background: #fdd; /* very light red background for stderr */
206 214 }
207 215
208 216 div.output_latex {
209 217 text-align: left;
210 218 color: black;
211 219 }
212 220
213 221 div.output_html {
214 222 }
215 223
216 224 div.output_png {
217 225 }
218 226
219 227 div.output_jpeg {
220 228 }
221 229
222 230 div.text_cell {
223 231 background-color: white;
224 232 padding: 5px 5px 5px 5px;
225 233 }
226 234
227 235 div.text_cell_input {
228 236 color: black;
229 237 border: 1px solid #ddd;
230 238 border-radius: 3px;
231 239 background: #f7f7f7;
232 240 }
233 241
234 242 div.text_cell_render {
235 243 font-family: "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
236 244 outline: none;
237 245 resize: none;
238 246 width: inherit;
239 247 border-style: none;
240 248 padding: 5px;
241 249 color: black;
242 250 }
243 251
244 252 .CodeMirror {
245 253 line-height: 1.231; /* Changed from 1em to our global default */
246 254 }
247 255
248 256 .CodeMirror-scroll {
249 257 height: auto; /* Changed to auto to autogrow */
250 258 /* The CodeMirror docs are a bit fuzzy on if overflow-y should be hidden or visible.*/
251 259 /* We have found that if it is visible, vertical scrollbars appear with font size changes.*/
252 260 overflow-y: hidden;
253 261 overflow-x: auto; /* Changed from auto to remove scrollbar */
254 262 }
255 263
256 264 /* CSS font colors for translated ANSI colors. */
257 265
258 266
259 267 .ansiblack {color: black;}
260 268 .ansired {color: darkred;}
261 269 .ansigreen {color: darkgreen;}
262 270 .ansiyellow {color: brown;}
263 271 .ansiblue {color: darkblue;}
264 272 .ansipurple {color: darkviolet;}
265 273 .ansicyan {color: steelblue;}
266 274 .ansigrey {color: grey;}
267 275 .ansibold {font-weight: bold;}
268 276
269 277 .completions , .tooltip {
270 278 position: absolute;
271 279 z-index: 10;
272 280 overflow: auto;
273 281 border: 1px solid black;
274 282 }
275 283
276 284 .completions select {
277 285 background: white;
278 286 outline: none;
279 287 border: none;
280 288 padding: 0px;
281 289 margin: 0px;
282 290 font-family: monospace;
283 291 }
284 292
285 293 @-moz-keyframes fadeIn {
286 294 from {opacity:0;}
287 295 to {opacity:1;}
288 296 }
289 297
290 298 @-webkit-keyframes fadeIn {
291 299 from {opacity:0;}
292 300 to {opacity:1;}
293 301 }
294 302
295 303 @keyframes fadeIn {
296 304 from {opacity:0;}
297 305 to {opacity:1;}
298 306 }
299 307
300 308 /*"close" "expand" and "Open in pager button" of
301 309 /* the tooltip*/
302 310 .tooltip a {
303 311 float:right;
304 312 }
305 313
306 314 /*properties of tooltip after "expand"*/
307 315 .bigtooltip {
308 316 height:30%;
309 317 }
310 318
311 319 /*properties of tooltip before "expand"*/
312 320 .smalltooltip {
313 321 text-overflow: ellipsis;
314 322 overflow: hidden;
315 323 height:15%;
316 324 }
317 325
318 326 .tooltip {
319 327 /*transition when "expand"ing tooltip */
320 328 -webkit-transition-property: height;
321 329 -webkit-transition-duration: 1s;
322 330 -moz-transition-property: height;
323 331 -moz-transition-duration: 1s;
324 332 transition-property: height;
325 333 transition-duration: 1s;
326 334 max-width:700px;
327 335 border-radius: 0px 10px 10px 10px;
328 336 box-shadow: 3px 3px 5px #999;
329 337 /*fade-in animation when inserted*/
330 338 -webkit-animation: fadeIn 200ms;
331 339 -moz-animation: fadeIn 200ms;
332 340 animation: fadeIn 200ms;
333 341 vertical-align: middle;
334 342 background: #FDFDD8;
335 343 outline: none;
336 344 padding: 3px;
337 345 margin: 0px;
338 346 font-family: monospace;
339 347 min-height:50px;
340 348 }
341 349
342 350 /*fixed part of the completion*/
343 351 .completions p b {
344 352 font-weight:bold;
345 353 }
346 354
347 355 .completions p {
348 356 background: #DDF;
349 357 /*outline: none;
350 358 padding: 0px;*/
351 359 border-bottom: black solid 1px;
352 360 padding: 1px;
353 361 font-family: monospace;
354 362 }
355 363
356 364 pre.dialog {
357 365 background-color: #f7f7f7;
358 366 border: 1px solid #ddd;
359 367 border-radius: 3px;
360 368 padding: 0.4em;
361 369 padding-left: 2em;
362 370 }
363 371
364 372 p.dialog {
365 373 padding : 0.2em;
366 374 }
367 375
368 376 .shortcut_key {
369 377 display: inline-block;
370 378 width: 15ex;
371 379 text-align: right;
372 380 font-family: monospace;
373 381 }
374 382
375 383 .shortcut_descr {
376 384 }
377 385
378 386 /* Word-wrap output correctly. This is the CSS3 spelling, though Firefox seems
379 387 to not honor it correctly. Webkit browsers (Chrome, rekonq, Safari) do.
380 388 */
381 389 pre, code, kbd, samp { white-space: pre-wrap; }
@@ -1,73 +1,77
1 1 /**
2 2 * Primary styles
3 3 *
4 4 * Author: IPython Development Team
5 5 */
6 6
7 7
8 8 body {
9 9 background-color: white;
10 10 /* This makes sure that the body covers the entire window and needs to
11 11 be in a different element than the display: box in wrapper below */
12 12 position: absolute;
13 13 left: 0px;
14 14 right: 0px;
15 15 top: 0px;
16 16 bottom: 0px;
17 overflow: hidden;
17 overflow: visible;
18 18 }
19 19
20 20
21 21 div#header {
22 22 /* Initially hidden to prevent FLOUC */
23 23 display: none;
24 24 position: relative;
25 25 height: 40px;
26 26 padding: 5px;
27 27 margin: 0px;
28 28 width: 100%;
29 29 }
30 30
31 31 span#ipython_notebook {
32 32 position: absolute;
33 33 padding: 2px 2px 2px 5px;
34 34 }
35 35
36 36 span#ipython_notebook h1 img {
37 37 font-family: Verdana, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
38 38 height: 24px;
39 39 text-decoration:none;
40 40 display: inline;
41 41 color: black;
42 42 }
43 43
44 div#main_app {
45 /* Initially hidden to prevent FLOUC */
44 #site {
45 width: 100%
46 46 display: none;
47 width: 100%;
48 position: relative;
49 47 }
50 48
51 49 /* We set the fonts by hand here to override the values in the theme */
52 50 .ui-widget {
53 51 font-family: "Lucinda Grande", "Lucinda Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
54 52 }
55 53
56 54 .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button {
57 55 font-family: "Lucinda Grande", "Lucinda Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
58 56 }
59 57
60 58 /* Smaller buttons */
61 59 .ui-button .ui-button-text {
62 60 padding: 0.2em 0.8em;
63 61 font-size: 77%;
64 62 }
65 63
64 input.ui-button {
65 padding: 0.3em 0.9em;
66 }
67
66 68 span#login_widget {
67 69 float: right;
68 70 }
69 71
70 /* generic class for hidden objects */
71 .hidden {
72 display: none;
72 .border-box-sizing {
73 box-sizing: border-box;
74 -moz-box-sizing: border-box;
75 -webkit-box-sizing: border-box;
73 76 }
77
@@ -1,82 +1,53
1 1
2 2 /**
3 3 * Primary styles
4 4 *
5 5 * Author: IPython Development Team
6 6 */
7 7
8
9 body {
10 background-color: white;
11 /* This makes sure that the body covers the entire window and needs to
12 be in a different element than the display: box in wrapper below */
13 position: absolute;
14 left: 0px;
15 right: 0px;
16 top: 0px;
17 bottom: 0px;
18 overflow: auto;
19 }
20
21 #left_panel {
8 #main_app {
9 width: 920px;
10 margin: auto;
22 11 }
23 12
24 #drop_zone {
25 height: 200px;
26 width: 200px
27 }
28
29 #content_panel {
30 width: 600px;
31 }
32
33 #content_toolbar {
13 #notebooks_toolbar {
34 14 padding: 5px;
35 15 height: 25px;
36 16 line-height: 25px;
37 17 }
38 18
39 #header_border {
40 width: 100%;
41 height: 2px;
42 }
43
44 #app_hbox {
45 width: 100%;
46 }
47
48 19 #drag_info {
49 20 float: left;
50 21 }
51 22
52 23 #notebooks_buttons {
53 24 float: right;
54 25 }
55 26
56 27 #project_name {
57 28 height: 25px;
58 29 line-height: 25px;
59 30 padding: 3px;
60 31 }
61 32
62 33 .notebook_item {
63 34 height: 25px;
64 35 line-height: 25px;
65 36 padding: 3px;
66 37 }
67 38
68 39 .notebook_item a {
69 40 text-decoration: none;
70 41 }
71 42
72 43 .item_buttons {
73 44 float: right;
74 45 }
75 46
76 47 .item_buttons .upload_button {
77 48 color: darkred;
78 49 }
79 50
80 51 .highlight_text {
81 52 color: blue;
82 53 }
1 NO CONTENT: file renamed from IPython/frontend/html/notebook/static/js/layout.js to IPython/frontend/html/notebook/static/js/layoutmanager.js
@@ -1,30 +1,22
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // On document ready
10 10 //============================================================================
11 11
12 12
13 13 $(document).ready(function () {
14 14
15 $('div#header').addClass('border-box-sizing');
16 $('div#header_border').addClass('border-box-sizing ui-widget ui-widget-content');
17
15 IPython.page = new IPython.Page();
16 $('input#login_submit').button();
18 17 $('div#main_app').addClass('border-box-sizing ui-widget');
19 $('div#app_hbox').addClass('hbox');
20
21 $('div#left_panel').addClass('box-flex');
22 $('div#right_panel').addClass('box-flex');
23 $('input#signin').button();
24
25 // These have display: none in the css file and are made visible here to prevent FLOUC.
26 $('div#header').css('display','block');
27 $('div#main_app').css('display','block');
18 IPython.page.show();
19 $('input#password_input').focus();
28 20
29 21 });
30 22
@@ -1,42 +1,44
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Login button
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var LoginWidget = function (selector) {
15 15 this.selector = selector;
16 16 if (this.selector !== undefined) {
17 17 this.element = $(selector);
18 18 this.style();
19 19 this.bind_events();
20 20 }
21 21 };
22 22
23 23 LoginWidget.prototype.style = function () {
24 24 this.element.find('button#logout').button();
25 25 this.element.find('button#login').button();
26 26 };
27
28
27 29 LoginWidget.prototype.bind_events = function () {
28 30 var that = this;
29 31 this.element.find("button#logout").click(function () {
30 32 window.location = "/logout";
31 33 });
32 34 this.element.find("button#login").click(function () {
33 35 window.location = "/login";
34 36 });
35 37 };
36 38
37 39 // Set module variables
38 40 IPython.LoginWidget = LoginWidget;
39 41
40 42 return IPython;
41 43
42 44 }(IPython));
@@ -1,251 +1,250
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // NotebookList
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var NotebookList = function (selector) {
15 15 this.selector = selector;
16 16 if (this.selector !== undefined) {
17 17 this.element = $(selector);
18 18 this.style();
19 19 this.bind_events();
20 20 }
21 21 };
22 22
23 23 NotebookList.prototype.style = function () {
24 this.element.addClass('ui-widget ui-widget-content');
25 24 $('div#project_name').addClass('ui-widget ui-widget-header');
26 25 };
27 26
28 27
29 28 NotebookList.prototype.bind_events = function () {
30 29 if (IPython.read_only){
31 30 return;
32 31 }
33 32 var that = this;
34 33 this.element.bind('dragover', function () {
35 34 return false;
36 35 });
37 36 this.element.bind('drop', function (event) {
38 37 var files = event.originalEvent.dataTransfer.files;
39 38 for (var i = 0, f; f = files[i]; i++) {
40 39 var reader = new FileReader();
41 40 reader.readAsText(f);
42 41 var fname = f.name.split('.');
43 42 var nbname = fname.slice(0,-1).join('.');
44 43 var nbformat = fname.slice(-1)[0];
45 44 if (nbformat === 'ipynb') {nbformat = 'json';};
46 45 if (nbformat === 'py' || nbformat === 'json') {
47 46 var item = that.new_notebook_item(0);
48 47 that.add_name_input(nbname, item);
49 48 item.data('nbformat', nbformat);
50 49 // Store the notebook item in the reader so we can use it later
51 50 // to know which item it belongs to.
52 51 $(reader).data('item', item);
53 52 reader.onload = function (event) {
54 53 var nbitem = $(event.target).data('item');
55 54 that.add_notebook_data(event.target.result, nbitem);
56 55 that.add_upload_button(nbitem);
57 56 };
58 57 };
59 58 }
60 59 return false;
61 60 });
62 61 };
63 62
64 63
65 64 NotebookList.prototype.load_list = function () {
66 65 var settings = {
67 66 processData : false,
68 67 cache : false,
69 68 type : "GET",
70 69 dataType : "json",
71 70 success : $.proxy(this.list_loaded, this)
72 71 };
73 72 var url = $('body').data('baseProjectUrl') + 'notebooks';
74 73 $.ajax(url, settings);
75 74 };
76 75
77 76
78 77 NotebookList.prototype.list_loaded = function (data, status, xhr) {
79 78 var len = data.length;
80 79 // Todo: remove old children
81 80 for (var i=0; i<len; i++) {
82 81 var notebook_id = data[i].notebook_id;
83 82 var nbname = data[i].name;
84 83 var item = this.new_notebook_item(i);
85 84 this.add_link(notebook_id, nbname, item);
86 85 if (!IPython.read_only){
87 86 // hide delete buttons when readonly
88 87 this.add_delete_button(item);
89 88 }
90 89 };
91 90 };
92 91
93 92
94 93 NotebookList.prototype.new_notebook_item = function (index) {
95 94 var item = $('<div/>');
96 95 item.addClass('notebook_item ui-widget ui-widget-content ui-helper-clearfix');
97 96 var item_name = $('<span/>').addClass('item_name');
98 97
99 98 item.append(item_name);
100 99 if (index === -1) {
101 100 this.element.append(item);
102 101 } else {
103 102 this.element.children().eq(index).after(item);
104 103 }
105 104 return item;
106 105 };
107 106
108 107
109 108 NotebookList.prototype.add_link = function (notebook_id, nbname, item) {
110 109 item.data('nbname', nbname);
111 110 item.data('notebook_id', notebook_id);
112 111 var new_item_name = $('<span/>').addClass('item_name');
113 112 new_item_name.append(
114 113 $('<a/>').
115 114 attr('href', $('body').data('baseProjectUrl')+notebook_id).
116 115 attr('target','_blank').
117 116 text(nbname)
118 117 );
119 118 var e = item.find('.item_name');
120 119 if (e.length === 0) {
121 120 item.append(new_item_name);
122 121 } else {
123 122 e.replaceWith(new_item_name);
124 123 };
125 124 };
126 125
127 126
128 127 NotebookList.prototype.add_name_input = function (nbname, item) {
129 128 item.data('nbname', nbname);
130 129 var new_item_name = $('<span/>').addClass('item_name');
131 130 new_item_name.append(
132 131 $('<input/>').addClass('ui-widget ui-widget-content').
133 132 attr('value', nbname).
134 133 attr('size', '30').
135 134 attr('type', 'text')
136 135 );
137 136 var e = item.find('.item_name');
138 137 if (e.length === 0) {
139 138 item.append(new_item_name);
140 139 } else {
141 140 e.replaceWith(new_item_name);
142 141 };
143 142 };
144 143
145 144
146 145 NotebookList.prototype.add_notebook_data = function (data, item) {
147 146 item.data('nbdata',data);
148 147 };
149 148
150 149
151 150 NotebookList.prototype.add_delete_button = function (item) {
152 151 var new_buttons = $('<span/>').addClass('item_buttons');
153 152 var delete_button = $('<button>Delete</button>').button().
154 153 click(function (e) {
155 154 // $(this) is the button that was clicked.
156 155 var that = $(this);
157 156 // We use the nbname and notebook_id from the parent notebook_item element's
158 157 // data because the outer scopes values change as we iterate through the loop.
159 158 var parent_item = that.parents('div.notebook_item');
160 159 var nbname = parent_item.data('nbname');
161 160 var notebook_id = parent_item.data('notebook_id');
162 161 var dialog = $('<div/>');
163 162 dialog.html('Are you sure you want to permanently delete the notebook: ' + nbname + '?');
164 163 parent_item.append(dialog);
165 164 dialog.dialog({
166 165 resizable: false,
167 166 modal: true,
168 167 title: "Delete notebook",
169 168 buttons : {
170 169 "Delete": function () {
171 170 var settings = {
172 171 processData : false,
173 172 cache : false,
174 173 type : "DELETE",
175 174 dataType : "json",
176 175 success : function (data, status, xhr) {
177 176 parent_item.remove();
178 177 }
179 178 };
180 179 var url = $('body').data('baseProjectUrl') + 'notebooks/' + notebook_id;
181 180 $.ajax(url, settings);
182 181 $(this).dialog('close');
183 182 },
184 183 "Cancel": function () {
185 184 $(this).dialog('close');
186 185 }
187 186 }
188 187 });
189 188 });
190 189 new_buttons.append(delete_button);
191 190 var e = item.find('.item_buttons');
192 191 if (e.length === 0) {
193 192 item.append(new_buttons);
194 193 } else {
195 194 e.replaceWith(new_buttons);
196 195 };
197 196 };
198 197
199 198
200 199 NotebookList.prototype.add_upload_button = function (item) {
201 200 var that = this;
202 201 var new_buttons = $('<span/>').addClass('item_buttons');
203 202 var upload_button = $('<button>Upload</button>').button().
204 203 click(function (e) {
205 204 var nbname = item.find('.item_name > input').attr('value');
206 205 var nbformat = item.data('nbformat');
207 206 var nbdata = item.data('nbdata');
208 207 var content_type = 'text/plain';
209 208 if (nbformat === 'json') {
210 209 content_type = 'application/json';
211 210 } else if (nbformat === 'py') {
212 211 content_type = 'application/x-python';
213 212 };
214 213 var settings = {
215 214 processData : false,
216 215 cache : false,
217 216 type : 'POST',
218 217 dataType : 'json',
219 218 data : nbdata,
220 219 headers : {'Content-Type': content_type},
221 220 success : function (data, status, xhr) {
222 221 that.add_link(data, nbname, item);
223 222 that.add_delete_button(item);
224 223 }
225 224 };
226 225
227 226 var qs = $.param({name:nbname, format:nbformat});
228 227 var url = $('body').data('baseProjectUrl') + 'notebooks?' + qs;
229 228 $.ajax(url, settings);
230 229 });
231 230 var cancel_button = $('<button>Cancel</button>').button().
232 231 click(function (e) {
233 232 item.remove();
234 233 });
235 234 upload_button.addClass('upload_button');
236 235 new_buttons.append(upload_button).append(cancel_button);
237 236 var e = item.find('.item_buttons');
238 237 if (e.length === 0) {
239 238 item.append(new_buttons);
240 239 } else {
241 240 e.replaceWith(new_buttons);
242 241 };
243 242 };
244 243
245 244
246 245 IPython.NotebookList = NotebookList;
247 246
248 247 return IPython;
249 248
250 249 }(IPython));
251 250
@@ -1,120 +1,119
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // On document ready
10 10 //============================================================================
11 11
12 12
13 13 $(document).ready(function () {
14 14 if (window.MathJax){
15 15 // MathJax loaded
16 16 MathJax.Hub.Config({
17 17 tex2jax: {
18 18 inlineMath: [ ['$','$'], ["\\(","\\)"] ],
19 19 displayMath: [ ['$$','$$'], ["\\[","\\]"] ]
20 20 },
21 21 displayAlign: 'left', // Change this to 'center' to center equations.
22 22 "HTML-CSS": {
23 23 styles: {'.MathJax_Display': {"margin": 0}}
24 24 }
25 25 });
26 26 }else if (window.mathjax_url != ""){
27 27 // Don't have MathJax, but should. Show dialog.
28 28 var dialog = $('<div></div>')
29 29 .append(
30 30 $("<p></p>").addClass('dialog').html(
31 31 "Math/LaTeX rendering will be disabled."
32 32 )
33 33 ).append(
34 34 $("<p></p>").addClass('dialog').html(
35 35 "If you have administrative access to the notebook server and" +
36 36 " a working internet connection, you can install a local copy" +
37 37 " of MathJax for offline use with the following command on the server" +
38 38 " at a Python or IPython prompt:"
39 39 )
40 40 ).append(
41 41 $("<pre></pre>").addClass('dialog').html(
42 42 ">>> from IPython.external import mathjax; mathjax.install_mathjax()"
43 43 )
44 44 ).append(
45 45 $("<p></p>").addClass('dialog').html(
46 46 "This will try to install MathJax into the IPython source directory."
47 47 )
48 48 ).append(
49 49 $("<p></p>").addClass('dialog').html(
50 50 "If IPython is installed to a location that requires" +
51 51 " administrative privileges to write, you will need to make this call as" +
52 52 " an administrator, via 'sudo'."
53 53 )
54 54 ).append(
55 55 $("<p></p>").addClass('dialog').html(
56 56 "When you start the notebook server, you can instruct it to disable MathJax support altogether:"
57 57 )
58 58 ).append(
59 59 $("<pre></pre>").addClass('dialog').html(
60 60 "$ ipython notebook --no-mathjax"
61 61 )
62 62 ).append(
63 63 $("<p></p>").addClass('dialog').html(
64 64 "which will prevent this dialog from appearing."
65 65 )
66 66 ).dialog({
67 67 title: "Failed to retrieve MathJax from '" + window.mathjax_url + "'",
68 68 width: "70%",
69 69 modal: true,
70 70 })
71 71 }else{
72 72 // No MathJax, but none expected. No dialog.
73 73 }
74 74
75 75
76 76 IPython.markdown_converter = new Markdown.Converter();
77 77 IPython.read_only = $('meta[name=read_only]').attr("content") == 'True';
78 78
79 79 $('div#header').addClass('border-box-sizing');
80 80 $('div#main_app').addClass('border-box-sizing ui-widget ui-widget-content');
81 81 $('div#notebook_panel').addClass('border-box-sizing ui-widget');
82 82
83 83 IPython.layout_manager = new IPython.LayoutManager();
84 84 IPython.pager = new IPython.Pager('div#pager', 'div#pager_splitter');
85 85 IPython.quick_help = new IPython.QuickHelp('span#quick_help_area');
86 86 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
87 87 IPython.notebook = new IPython.Notebook('div#notebook');
88 88 IPython.save_widget = new IPython.SaveWidget('span#save_widget');
89 89 IPython.menubar = new IPython.MenuBar('#menubar')
90 90 IPython.toolbar = new IPython.ToolBar('#toolbar')
91 91 IPython.notification_widget = new IPython.NotificationWidget('#notification')
92 92
93 93 IPython.layout_manager.do_resize();
94 94
95 95 // These have display: none in the css file and are made visible here to prevent FLOUC.
96 96 $('div#header').css('display','block');
97 97
98 98 if(IPython.read_only){
99 99 // hide various elements from read-only view
100 100 $('div#pager').remove();
101 101 $('div#pager_splitter').remove();
102 $('span#login_widget').removeClass('hidden');
103 102
104 103 // set the notebook name field as not modifiable
105 104 $('#notebook_name').attr('disabled','disabled')
106 105 }
107 106
108 107 $('div#menubar').css('display','block');
109 108 $('div#toolbar').css('display','block');
110 109 $('div#main_app').css('display','block');
111 110
112 111 IPython.layout_manager.do_resize();
113 112 $([IPython.events]).on('notebook_loaded.Notebook', function () {
114 113 IPython.layout_manager.do_resize();
115 114 IPython.save_widget.update_url();
116 115 })
117 116 IPython.notebook.load_notebook($('body').data('notebookId'));
118 117
119 118 });
120 119
@@ -1,42 +1,32
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // On document ready
10 10 //============================================================================
11 11
12 12
13 13 $(document).ready(function () {
14 14
15 $('div#header').addClass('border-box-sizing');
16 $('div#header_border').addClass('border-box-sizing ui-widget ui-widget-content');
15 IPython.page = new IPython.Page();
17 16
18 17 $('div#main_app').addClass('border-box-sizing ui-widget');
19 $('div#app_hbox').addClass('hbox');
20
21 $('div#content_toolbar').addClass('ui-widget ui-helper-clearfix');
22
18 $('div#notebooks_toolbar').addClass('ui-widget ui-helper-clearfix');
23 19 $('#new_notebook').button().click(function (e) {
24 20 window.open($('body').data('baseProjectUrl')+'new');
25 21 });
26 22
27 $('div#left_panel').addClass('box-flex');
28 $('div#right_panel').addClass('box-flex');
29
30 IPython.read_only = $('meta[name=read_only]').attr("content") == 'True';
23 IPython.read_only = $('body').data('readOnly') === 'True';
31 24 IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
32 25 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
33 26
34 27 IPython.notebook_list.load_list();
35 28
36 // These have display: none in the css file and are made visible here to prevent FLOUC.
37 $('div#header').css('display','block');
38 $('div#main_app').css('display','block');
39
29 IPython.page.show();
40 30
41 31 });
42 32
@@ -1,26 +1,42
1 {% extends layout.html %}
1 {% extends page.html %}
2 2
3 {% block content_panel %}
3 {% block stylesheet %}
4 4
5 {% if login_available %}
5 <link rel="stylesheet" href="{{static_url("css/login.css") }}" type="text/css"/>
6
7 {% end %}
8
9
10 {% block login_widget %}
11 {% end %}
12
13
14 {% block site %}
6 15
16 <div id="main_app">
17
18 {% if login_available %}
7 19 <form action="/login?next={{url_escape(next)}}" method="post">
8 Password: <input type="password" name="password" id="focus">
9 <input type="submit" value="Sign in" id="signin">
20 Password: <input type="password" name="password" id="password_input">
21 <input type="submit" value="Log in" id="login_submit">
10 22 </form>
11
12 23 {% end %}
13 24
25 {% if message %}
26 {% for key in message %}
27 <div class="message {{key}}">
28 {{message[key]}}
29 </div>
30 {% end %}
14 31 {% end %}
15 32
16 {% block login_widget %}
33 <div/>
34
17 35 {% end %}
18 36
37
19 38 {% block script %}
20 <script type="text/javascript">
21 $(document).ready(function() {
22 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
23 $('#focus').focus();
24 });
25 </script>
39
40 <script src="{{static_url("js/loginmain.js") }}" type="text/javascript" charset="utf-8"></script>
41
26 42 {% end %}
@@ -1,28 +1,40
1 {% extends layout.html %}
1 {% extends page.html %}
2 2
3 {% block content_panel %}
4 <ul>
5 {% if read_only or not login_available %}
3 {% block stylesheet %}
6 4
7 Proceed to the <a href="/">list of notebooks</a>.</li>
5 <link rel="stylesheet" href="{{static_url("css/logout.css") }}" type="text/css"/>
8 6
9 {% else %}
7 {% end %}
10 8
11 Proceed to the <a href="/login">login page</a>.</li>
12 9
10 {% block login_widget %}
13 11 {% end %}
14 12
15 </ul>
13 {% block site %}
16 14
15 <div id="main_app">
16
17 {% if message %}
18 {% for key in message %}
19 <div class="message {{key}}">
20 {{message[key]}}
21 </div>
22 {% end %}
17 23 {% end %}
18 24
19 {% block login_widget %}
25 {% if read_only or not login_available %}
26 Proceed to the <a href="/">dashboard</a>.
27 {% else %}
28 Proceed to the <a href="/login">login page</a>.
29 {% end %}
30
31
32 <div/>
33
20 34 {% end %}
21 35
22 36 {% block script %}
23 <script type="text/javascript">
24 $(document).ready(function() {
25 IPython.login_widget = new IPython.LoginWidget('span#login_widget');
26 });
27 </script>
37
38 <script src="{{static_url("js/logoutmain.js") }}" type="text/javascript" charset="utf-8"></script>
39
28 40 {% end %}
@@ -1,86 +1,58
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 <link rel="stylesheet" href="{{static_url("css/layout.css") }}" type="text/css" />
12 <link rel="stylesheet" href="{{static_url("css/base.css") }}" type="text/css"/>
11 <link rel="stylesheet" href="{{static_url("css/fbm.css") }}" type="text/css" />
12 <link rel="stylesheet" href="{{static_url("css/page.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 <span id="ipython_notebook"><h1><img src='{{static_url("ipynblogo.png") }}' alt='IPython Notebook'/></h1></span>
24 <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 25
26 26 {% block login_widget %}
27 27
28 28 <span id="login_widget">
29 29 {% if logged_in %}
30 30 <button id="logout">Logout</button>
31 31 {% elif login_available and not logged_in %}
32 32 <button id="login">Login</button>
33 33 {% end %}
34 34 </span>
35 35
36 36 {% end %}
37 37
38 38 {% block header %}
39 39 {% end %}
40 40 </div>
41 41
42 <div id="header_border"></div>
43
44 <div id="main_app">
45
46 <div id="app_hbox">
47
48 <div id="left_panel">
49 {% block left_panel %}
50 {% end %}
51 </div>
52
53 <div id="content_panel">
54 {% if message %}
55
56 {% for key in message %}
57 <div class="message {{key}}">
58 {{message[key]}}
59 </div>
60 {% end %}
42 <div id="site">
43 {% block site %}
61 44 {% end %}
62
63 {% block content_panel %}
64 {% end %}
65 </div>
66 <div id="right_panel">
67 {% block right_panel %}
68 {% end %}
69 </div>
70
71 </div>
72
73 45 </div>
74 46
75 47 <script src="{{static_url("jquery/js/jquery-1.7.1.min.js") }}" type="text/javascript" charset="utf-8"></script>
76 48 <script src="{{static_url("jquery/js/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
77 49 <script src="{{static_url("js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
78 <script src="{{static_url("js/loginmain.js") }}" type="text/javascript" charset="utf-8"></script>
50 <script src="{{static_url("js/page.js") }}" type="text/javascript" charset="utf-8"></script>
79 51 <script src="{{static_url("js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
80 52
81 53 {% block script %}
82 54 {% end %}
83 55
84 56 </body>
85 57
86 58 </html>
@@ -1,43 +1,45
1 {% extends layout.html %}
1 {% extends page.html %}
2 2
3 {% block title %}
4 IPython Dashboard
5 {% end %}
3 {% block title %}IPython Dashboard{% end %}
6 4
7 5 {% block stylesheet %}
8 6 <link rel="stylesheet" href="{{static_url("css/projectdashboard.css") }}" type="text/css" />
9 7 {% end %}
10 8
11 {% block meta %}
12 <meta name="read_only" content="{{read_only}}"/>
13 {% end %}
14 9
15 10 {% block params %}
16 11 data-project={{project}}
17 12 data-base-project-url={{base_project_url}}
18 13 data-base-kernel-url={{base_kernel_url}}
14 data-read-only={{read_only}}
19 15 {% end %}
20 16
21 {% block content_panel %}
17 {% block site %}
18
19 <div id="main_app">
20
22 21 {% if logged_in or not read_only %}
23 22
24 <div id="content_toolbar">
23 <div id="notebooks_toolbar">
25 24 <span id="drag_info">Drag files onto the list to import
26 25 notebooks.</span>
27 26
28 27 <span id="notebooks_buttons">
29 28 <button id="new_notebook">New Notebook</button>
30 29 </span>
31 30 </div>
32 31
33 32 {% end %}
34 33
35 34 <div id="notebook_list">
36 35 <div id="project_name"><h2>{{project}}</h2></div>
37 36 </div>
37
38 </div>
39
38 40 {% end %}
39 41
40 42 {% block script %}
41 43 <script src="{{static_url("js/notebooklist.js") }}" type="text/javascript" charset="utf-8"></script>
42 44 <script src="{{static_url("js/projectdashboardmain.js") }}" type="text/javascript" charset="utf-8"></script>
43 45 {% end %}
General Comments 0
You need to be logged in to leave comments. Login now