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