##// END OF EJS Templates
allow draft76 websockets (Safari)...
MinRK -
Show More
@@ -1,686 +1,694 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 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 214 message=message
215 215 )
216 216
217 217 def get(self):
218 218 if self.current_user:
219 219 self.redirect(self.get_argument('next', default='/'))
220 220 else:
221 221 self._render()
222 222
223 223 def post(self):
224 224 pwd = self.get_argument('password', default=u'')
225 225 if self.application.password:
226 226 if passwd_check(self.application.password, pwd):
227 227 self.set_secure_cookie('username', str(uuid.uuid4()))
228 228 else:
229 229 self._render(message={'error': 'Invalid password'})
230 230 return
231 231
232 232 self.redirect(self.get_argument('next', default='/'))
233 233
234 234
235 235 class LogoutHandler(AuthenticatedHandler):
236 236
237 237 def get(self):
238 238 self.clear_cookie('username')
239 239 if self.login_available:
240 240 message = {'info': 'Successfully logged out.'}
241 241 else:
242 242 message = {'warning': 'Cannot log out. Notebook authentication '
243 243 'is disabled.'}
244 244
245 245 self.render('logout.html',
246 246 read_only=self.read_only,
247 247 logged_in=self.logged_in,
248 248 login_available=self.login_available,
249 249 message=message)
250 250
251 251
252 252 class NewHandler(AuthenticatedHandler):
253 253
254 254 @web.authenticated
255 255 def get(self):
256 256 nbm = self.application.notebook_manager
257 257 project = nbm.notebook_dir
258 258 notebook_id = nbm.new_notebook()
259 259 self.render(
260 260 'notebook.html', project=project,
261 261 notebook_id=notebook_id,
262 262 base_project_url=self.application.ipython_app.base_project_url,
263 263 base_kernel_url=self.application.ipython_app.base_kernel_url,
264 264 kill_kernel=False,
265 265 read_only=False,
266 266 logged_in=self.logged_in,
267 267 login_available=self.login_available,
268 268 mathjax_url=self.application.ipython_app.mathjax_url,
269 269 )
270 270
271 271
272 272 class NamedNotebookHandler(AuthenticatedHandler):
273 273
274 274 @authenticate_unless_readonly
275 275 def get(self, notebook_id):
276 276 nbm = self.application.notebook_manager
277 277 project = nbm.notebook_dir
278 278 if not nbm.notebook_exists(notebook_id):
279 279 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
280 280
281 281 self.render(
282 282 'notebook.html', project=project,
283 283 notebook_id=notebook_id,
284 284 base_project_url=self.application.ipython_app.base_project_url,
285 285 base_kernel_url=self.application.ipython_app.base_kernel_url,
286 286 kill_kernel=False,
287 287 read_only=self.read_only,
288 288 logged_in=self.logged_in,
289 289 login_available=self.login_available,
290 290 mathjax_url=self.application.ipython_app.mathjax_url,
291 291 )
292 292
293 293
294 294 class PrintNotebookHandler(AuthenticatedHandler):
295 295
296 296 @authenticate_unless_readonly
297 297 def get(self, notebook_id):
298 298 nbm = self.application.notebook_manager
299 299 project = nbm.notebook_dir
300 300 if not nbm.notebook_exists(notebook_id):
301 301 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
302 302
303 303 self.render(
304 304 'printnotebook.html', project=project,
305 305 notebook_id=notebook_id,
306 306 base_project_url=self.application.ipython_app.base_project_url,
307 307 base_kernel_url=self.application.ipython_app.base_kernel_url,
308 308 kill_kernel=False,
309 309 read_only=self.read_only,
310 310 logged_in=self.logged_in,
311 311 login_available=self.login_available,
312 312 mathjax_url=self.application.ipython_app.mathjax_url,
313 313 )
314 314
315 315 #-----------------------------------------------------------------------------
316 316 # Kernel handlers
317 317 #-----------------------------------------------------------------------------
318 318
319 319
320 320 class MainKernelHandler(AuthenticatedHandler):
321 321
322 322 @web.authenticated
323 323 def get(self):
324 324 km = self.application.kernel_manager
325 325 self.finish(jsonapi.dumps(km.kernel_ids))
326 326
327 327 @web.authenticated
328 328 def post(self):
329 329 km = self.application.kernel_manager
330 330 notebook_id = self.get_argument('notebook', default=None)
331 331 kernel_id = km.start_kernel(notebook_id)
332 332 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
333 333 self.set_header('Location', '/'+kernel_id)
334 334 self.finish(jsonapi.dumps(data))
335 335
336 336
337 337 class KernelHandler(AuthenticatedHandler):
338 338
339 339 SUPPORTED_METHODS = ('DELETE')
340 340
341 341 @web.authenticated
342 342 def delete(self, kernel_id):
343 343 km = self.application.kernel_manager
344 344 km.kill_kernel(kernel_id)
345 345 self.set_status(204)
346 346 self.finish()
347 347
348 348
349 349 class KernelActionHandler(AuthenticatedHandler):
350 350
351 351 @web.authenticated
352 352 def post(self, kernel_id, action):
353 353 km = self.application.kernel_manager
354 354 if action == 'interrupt':
355 355 km.interrupt_kernel(kernel_id)
356 356 self.set_status(204)
357 357 if action == 'restart':
358 358 new_kernel_id = km.restart_kernel(kernel_id)
359 359 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
360 360 self.set_header('Location', '/'+new_kernel_id)
361 361 self.write(jsonapi.dumps(data))
362 362 self.finish()
363 363
364 364
365 365 class ZMQStreamHandler(websocket.WebSocketHandler):
366 366
367 367 def _reserialize_reply(self, msg_list):
368 368 """Reserialize a reply message using JSON.
369 369
370 370 This takes the msg list from the ZMQ socket, unserializes it using
371 371 self.session and then serializes the result using JSON. This method
372 372 should be used by self._on_zmq_reply to build messages that can
373 373 be sent back to the browser.
374 374 """
375 375 idents, msg_list = self.session.feed_identities(msg_list)
376 376 msg = self.session.unserialize(msg_list)
377 377 try:
378 378 msg['header'].pop('date')
379 379 except KeyError:
380 380 pass
381 381 try:
382 382 msg['parent_header'].pop('date')
383 383 except KeyError:
384 384 pass
385 385 msg.pop('buffers')
386 386 return jsonapi.dumps(msg)
387 387
388 388 def _on_zmq_reply(self, msg_list):
389 389 try:
390 390 msg = self._reserialize_reply(msg_list)
391 391 except:
392 392 self.application.log.critical("Malformed message: %r" % msg_list)
393 393 else:
394 394 self.write_message(msg)
395 395
396 def allow_draft76(self):
397 """Allow draft 76, until browsers such as Safari update to RFC 6455.
398
399 This has been disabled by default in tornado in release 2.2.0, and
400 support will be removed in later versions.
401 """
402 return True
403
396 404
397 405 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
398 406
399 407 def open(self, kernel_id):
400 408 self.kernel_id = kernel_id.decode('ascii')
401 409 try:
402 410 cfg = self.application.ipython_app.config
403 411 except AttributeError:
404 412 # protect from the case where this is run from something other than
405 413 # the notebook app:
406 414 cfg = None
407 415 self.session = Session(config=cfg)
408 416 self.save_on_message = self.on_message
409 417 self.on_message = self.on_first_message
410 418
411 419 def get_current_user(self):
412 420 user_id = self.get_secure_cookie("username")
413 421 if user_id == '' or (user_id is None and not self.application.password):
414 422 user_id = 'anonymous'
415 423 return user_id
416 424
417 425 def _inject_cookie_message(self, msg):
418 426 """Inject the first message, which is the document cookie,
419 427 for authentication."""
420 428 if isinstance(msg, unicode):
421 429 # Cookie can't constructor doesn't accept unicode strings for some reason
422 430 msg = msg.encode('utf8', 'replace')
423 431 try:
424 432 self.request._cookies = Cookie.SimpleCookie(msg)
425 433 except:
426 434 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
427 435
428 436 def on_first_message(self, msg):
429 437 self._inject_cookie_message(msg)
430 438 if self.get_current_user() is None:
431 439 logging.warn("Couldn't authenticate WebSocket connection")
432 440 raise web.HTTPError(403)
433 441 self.on_message = self.save_on_message
434 442
435 443
436 444 class IOPubHandler(AuthenticatedZMQStreamHandler):
437 445
438 446 def initialize(self, *args, **kwargs):
439 447 self._kernel_alive = True
440 448 self._beating = False
441 449 self.iopub_stream = None
442 450 self.hb_stream = None
443 451
444 452 def on_first_message(self, msg):
445 453 try:
446 454 super(IOPubHandler, self).on_first_message(msg)
447 455 except web.HTTPError:
448 456 self.close()
449 457 return
450 458 km = self.application.kernel_manager
451 459 self.time_to_dead = km.time_to_dead
452 460 self.first_beat = km.first_beat
453 461 kernel_id = self.kernel_id
454 462 try:
455 463 self.iopub_stream = km.create_iopub_stream(kernel_id)
456 464 self.hb_stream = km.create_hb_stream(kernel_id)
457 465 except web.HTTPError:
458 466 # WebSockets don't response to traditional error codes so we
459 467 # close the connection.
460 468 if not self.stream.closed():
461 469 self.stream.close()
462 470 self.close()
463 471 else:
464 472 self.iopub_stream.on_recv(self._on_zmq_reply)
465 473 self.start_hb(self.kernel_died)
466 474
467 475 def on_message(self, msg):
468 476 pass
469 477
470 478 def on_close(self):
471 479 # This method can be called twice, once by self.kernel_died and once
472 480 # from the WebSocket close event. If the WebSocket connection is
473 481 # closed before the ZMQ streams are setup, they could be None.
474 482 self.stop_hb()
475 483 if self.iopub_stream is not None and not self.iopub_stream.closed():
476 484 self.iopub_stream.on_recv(None)
477 485 self.iopub_stream.close()
478 486 if self.hb_stream is not None and not self.hb_stream.closed():
479 487 self.hb_stream.close()
480 488
481 489 def start_hb(self, callback):
482 490 """Start the heartbeating and call the callback if the kernel dies."""
483 491 if not self._beating:
484 492 self._kernel_alive = True
485 493
486 494 def ping_or_dead():
487 495 self.hb_stream.flush()
488 496 if self._kernel_alive:
489 497 self._kernel_alive = False
490 498 self.hb_stream.send(b'ping')
491 499 # flush stream to force immediate socket send
492 500 self.hb_stream.flush()
493 501 else:
494 502 try:
495 503 callback()
496 504 except:
497 505 pass
498 506 finally:
499 507 self.stop_hb()
500 508
501 509 def beat_received(msg):
502 510 self._kernel_alive = True
503 511
504 512 self.hb_stream.on_recv(beat_received)
505 513 loop = ioloop.IOLoop.instance()
506 514 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
507 515 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
508 516 self._beating= True
509 517
510 518 def _really_start_hb(self):
511 519 """callback for delayed heartbeat start
512 520
513 521 Only start the hb loop if we haven't been closed during the wait.
514 522 """
515 523 if self._beating and not self.hb_stream.closed():
516 524 self._hb_periodic_callback.start()
517 525
518 526 def stop_hb(self):
519 527 """Stop the heartbeating and cancel all related callbacks."""
520 528 if self._beating:
521 529 self._beating = False
522 530 self._hb_periodic_callback.stop()
523 531 if not self.hb_stream.closed():
524 532 self.hb_stream.on_recv(None)
525 533
526 534 def kernel_died(self):
527 535 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
528 536 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
529 537 self.write_message(
530 538 {'header': {'msg_type': 'status'},
531 539 'parent_header': {},
532 540 'content': {'execution_state':'dead'}
533 541 }
534 542 )
535 543 self.on_close()
536 544
537 545
538 546 class ShellHandler(AuthenticatedZMQStreamHandler):
539 547
540 548 def initialize(self, *args, **kwargs):
541 549 self.shell_stream = None
542 550
543 551 def on_first_message(self, msg):
544 552 try:
545 553 super(ShellHandler, self).on_first_message(msg)
546 554 except web.HTTPError:
547 555 self.close()
548 556 return
549 557 km = self.application.kernel_manager
550 558 self.max_msg_size = km.max_msg_size
551 559 kernel_id = self.kernel_id
552 560 try:
553 561 self.shell_stream = km.create_shell_stream(kernel_id)
554 562 except web.HTTPError:
555 563 # WebSockets don't response to traditional error codes so we
556 564 # close the connection.
557 565 if not self.stream.closed():
558 566 self.stream.close()
559 567 self.close()
560 568 else:
561 569 self.shell_stream.on_recv(self._on_zmq_reply)
562 570
563 571 def on_message(self, msg):
564 572 if len(msg) < self.max_msg_size:
565 573 msg = jsonapi.loads(msg)
566 574 self.session.send(self.shell_stream, msg)
567 575
568 576 def on_close(self):
569 577 # Make sure the stream exists and is not already closed.
570 578 if self.shell_stream is not None and not self.shell_stream.closed():
571 579 self.shell_stream.close()
572 580
573 581
574 582 #-----------------------------------------------------------------------------
575 583 # Notebook web service handlers
576 584 #-----------------------------------------------------------------------------
577 585
578 586 class NotebookRootHandler(AuthenticatedHandler):
579 587
580 588 @authenticate_unless_readonly
581 589 def get(self):
582 590
583 591 nbm = self.application.notebook_manager
584 592 files = nbm.list_notebooks()
585 593 self.finish(jsonapi.dumps(files))
586 594
587 595 @web.authenticated
588 596 def post(self):
589 597 nbm = self.application.notebook_manager
590 598 body = self.request.body.strip()
591 599 format = self.get_argument('format', default='json')
592 600 name = self.get_argument('name', default=None)
593 601 if body:
594 602 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
595 603 else:
596 604 notebook_id = nbm.new_notebook()
597 605 self.set_header('Location', '/'+notebook_id)
598 606 self.finish(jsonapi.dumps(notebook_id))
599 607
600 608
601 609 class NotebookHandler(AuthenticatedHandler):
602 610
603 611 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
604 612
605 613 @authenticate_unless_readonly
606 614 def get(self, notebook_id):
607 615 nbm = self.application.notebook_manager
608 616 format = self.get_argument('format', default='json')
609 617 last_mod, name, data = nbm.get_notebook(notebook_id, format)
610 618
611 619 if format == u'json':
612 620 self.set_header('Content-Type', 'application/json')
613 621 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
614 622 elif format == u'py':
615 623 self.set_header('Content-Type', 'application/x-python')
616 624 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
617 625 self.set_header('Last-Modified', last_mod)
618 626 self.finish(data)
619 627
620 628 @web.authenticated
621 629 def put(self, notebook_id):
622 630 nbm = self.application.notebook_manager
623 631 format = self.get_argument('format', default='json')
624 632 name = self.get_argument('name', default=None)
625 633 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
626 634 self.set_status(204)
627 635 self.finish()
628 636
629 637 @web.authenticated
630 638 def delete(self, notebook_id):
631 639 nbm = self.application.notebook_manager
632 640 nbm.delete_notebook(notebook_id)
633 641 self.set_status(204)
634 642 self.finish()
635 643
636 644
637 645 class NotebookCopyHandler(AuthenticatedHandler):
638 646
639 647 @web.authenticated
640 648 def get(self, notebook_id):
641 649 nbm = self.application.notebook_manager
642 650 project = nbm.notebook_dir
643 651 notebook_id = nbm.copy_notebook(notebook_id)
644 652 self.render(
645 653 'notebook.html', project=project,
646 654 notebook_id=notebook_id,
647 655 base_project_url=self.application.ipython_app.base_project_url,
648 656 base_kernel_url=self.application.ipython_app.base_kernel_url,
649 657 kill_kernel=False,
650 658 read_only=False,
651 659 logged_in=self.logged_in,
652 660 login_available=self.login_available,
653 661 mathjax_url=self.application.ipython_app.mathjax_url,
654 662 )
655 663
656 664 #-----------------------------------------------------------------------------
657 665 # RST web service handlers
658 666 #-----------------------------------------------------------------------------
659 667
660 668
661 669 class RSTHandler(AuthenticatedHandler):
662 670
663 671 @web.authenticated
664 672 def post(self):
665 673 if publish_string is None:
666 674 raise web.HTTPError(503, u'docutils not available')
667 675 body = self.request.body.strip()
668 676 source = body
669 677 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
670 678 defaults = {'file_insertion_enabled': 0,
671 679 'raw_enabled': 0,
672 680 '_disable_config': 1,
673 681 'stylesheet_path': 0
674 682 # 'template': template_path
675 683 }
676 684 try:
677 685 html = publish_string(source, writer_name='html',
678 686 settings_overrides=defaults
679 687 )
680 688 except:
681 689 raise web.HTTPError(400, u'Invalid RST')
682 690 print html
683 691 self.set_header('Content-Type', 'text/html')
684 692 self.finish(html)
685 693
686 694
General Comments 0
You need to be logged in to leave comments. Login now