##// END OF EJS Templates
Merge pull request #4796 from minrk/update-components...
Matthias Bussonnier -
r14217:3c647a2b merge
parent child Browse files
Show More
@@ -1,416 +1,410 b''
1 1 """Base 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) 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
20 20 import functools
21 21 import json
22 22 import logging
23 23 import os
24 24 import re
25 25 import stat
26 26 import sys
27 27 import traceback
28 28 try:
29 29 # py3
30 30 from http.client import responses
31 31 except ImportError:
32 32 from httplib import responses
33 33
34 34 from jinja2 import TemplateNotFound
35 35 from tornado import web
36 36
37 37 try:
38 38 from tornado.log import app_log
39 39 except ImportError:
40 40 app_log = logging.getLogger()
41 41
42 42 from IPython.config import Application
43 43 from IPython.utils.path import filefind
44 44 from IPython.utils.py3compat import string_types
45 45
46 46 # UF_HIDDEN is a stat flag not defined in the stat module.
47 47 # It is used by BSD to indicate hidden files.
48 48 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
49 49
50 50 #-----------------------------------------------------------------------------
51 51 # Top-level handlers
52 52 #-----------------------------------------------------------------------------
53 53 non_alphanum = re.compile(r'[^A-Za-z0-9]')
54 54
55 55 class AuthenticatedHandler(web.RequestHandler):
56 56 """A RequestHandler with an authenticated user."""
57 57
58 58 def clear_login_cookie(self):
59 59 self.clear_cookie(self.cookie_name)
60 60
61 61 def get_current_user(self):
62 62 user_id = self.get_secure_cookie(self.cookie_name)
63 63 # For now the user_id should not return empty, but it could eventually
64 64 if user_id == '':
65 65 user_id = 'anonymous'
66 66 if user_id is None:
67 67 # prevent extra Invalid cookie sig warnings:
68 68 self.clear_login_cookie()
69 69 if not self.login_available:
70 70 user_id = 'anonymous'
71 71 return user_id
72 72
73 73 @property
74 74 def cookie_name(self):
75 75 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
76 76 self.request.host
77 77 ))
78 78 return self.settings.get('cookie_name', default_cookie_name)
79 79
80 80 @property
81 81 def password(self):
82 82 """our password"""
83 83 return self.settings.get('password', '')
84 84
85 85 @property
86 86 def logged_in(self):
87 87 """Is a user currently logged in?
88 88
89 89 """
90 90 user = self.get_current_user()
91 91 return (user and not user == 'anonymous')
92 92
93 93 @property
94 94 def login_available(self):
95 95 """May a user proceed to log in?
96 96
97 97 This returns True if login capability is available, irrespective of
98 98 whether the user is already logged in or not.
99 99
100 100 """
101 101 return bool(self.settings.get('password', ''))
102 102
103 103
104 104 class IPythonHandler(AuthenticatedHandler):
105 105 """IPython-specific extensions to authenticated handling
106 106
107 107 Mostly property shortcuts to IPython-specific settings.
108 108 """
109 109
110 110 @property
111 111 def config(self):
112 112 return self.settings.get('config', None)
113 113
114 114 @property
115 115 def log(self):
116 116 """use the IPython log by default, falling back on tornado's logger"""
117 117 if Application.initialized():
118 118 return Application.instance().log
119 119 else:
120 120 return app_log
121 121
122 @property
123 def use_less(self):
124 """Use less instead of css in templates"""
125 return self.settings.get('use_less', False)
126
127 122 #---------------------------------------------------------------
128 123 # URLs
129 124 #---------------------------------------------------------------
130 125
131 126 @property
132 127 def ws_url(self):
133 128 """websocket url matching the current request
134 129
135 130 By default, this is just `''`, indicating that it should match
136 131 the same host, protocol, port, etc.
137 132 """
138 133 return self.settings.get('websocket_url', '')
139 134
140 135 @property
141 136 def mathjax_url(self):
142 137 return self.settings.get('mathjax_url', '')
143 138
144 139 @property
145 140 def base_project_url(self):
146 141 return self.settings.get('base_project_url', '/')
147 142
148 143 @property
149 144 def base_kernel_url(self):
150 145 return self.settings.get('base_kernel_url', '/')
151 146
152 147 #---------------------------------------------------------------
153 148 # Manager objects
154 149 #---------------------------------------------------------------
155 150
156 151 @property
157 152 def kernel_manager(self):
158 153 return self.settings['kernel_manager']
159 154
160 155 @property
161 156 def notebook_manager(self):
162 157 return self.settings['notebook_manager']
163 158
164 159 @property
165 160 def cluster_manager(self):
166 161 return self.settings['cluster_manager']
167 162
168 163 @property
169 164 def session_manager(self):
170 165 return self.settings['session_manager']
171 166
172 167 @property
173 168 def project_dir(self):
174 169 return self.notebook_manager.notebook_dir
175 170
176 171 #---------------------------------------------------------------
177 172 # template rendering
178 173 #---------------------------------------------------------------
179 174
180 175 def get_template(self, name):
181 176 """Return the jinja template object for a given name"""
182 177 return self.settings['jinja2_env'].get_template(name)
183 178
184 179 def render_template(self, name, **ns):
185 180 ns.update(self.template_namespace)
186 181 template = self.get_template(name)
187 182 return template.render(**ns)
188 183
189 184 @property
190 185 def template_namespace(self):
191 186 return dict(
192 187 base_project_url=self.base_project_url,
193 188 base_kernel_url=self.base_kernel_url,
194 189 logged_in=self.logged_in,
195 190 login_available=self.login_available,
196 use_less=self.use_less,
197 191 static_url=self.static_url,
198 192 )
199 193
200 194 def get_json_body(self):
201 195 """Return the body of the request as JSON data."""
202 196 if not self.request.body:
203 197 return None
204 198 # Do we need to call body.decode('utf-8') here?
205 199 body = self.request.body.strip().decode(u'utf-8')
206 200 try:
207 201 model = json.loads(body)
208 202 except Exception:
209 203 self.log.debug("Bad JSON: %r", body)
210 204 self.log.error("Couldn't parse JSON", exc_info=True)
211 205 raise web.HTTPError(400, u'Invalid JSON in body of request')
212 206 return model
213 207
214 208 def get_error_html(self, status_code, **kwargs):
215 209 """render custom error pages"""
216 210 exception = kwargs.get('exception')
217 211 message = ''
218 212 status_message = responses.get(status_code, 'Unknown HTTP Error')
219 213 if exception:
220 214 # get the custom message, if defined
221 215 try:
222 216 message = exception.log_message % exception.args
223 217 except Exception:
224 218 pass
225 219
226 220 # construct the custom reason, if defined
227 221 reason = getattr(exception, 'reason', '')
228 222 if reason:
229 223 status_message = reason
230 224
231 225 # build template namespace
232 226 ns = dict(
233 227 status_code=status_code,
234 228 status_message=status_message,
235 229 message=message,
236 230 exception=exception,
237 231 )
238 232
239 233 # render the template
240 234 try:
241 235 html = self.render_template('%s.html' % status_code, **ns)
242 236 except TemplateNotFound:
243 237 self.log.debug("No template for %d", status_code)
244 238 html = self.render_template('error.html', **ns)
245 239 return html
246 240
247 241
248 242 class Template404(IPythonHandler):
249 243 """Render our 404 template"""
250 244 def prepare(self):
251 245 raise web.HTTPError(404)
252 246
253 247
254 248 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
255 249 """static files should only be accessible when logged in"""
256 250
257 251 @web.authenticated
258 252 def get(self, path):
259 253 if os.path.splitext(path)[1] == '.ipynb':
260 254 name = os.path.basename(path)
261 255 self.set_header('Content-Type', 'application/json')
262 256 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
263 257
264 258 return web.StaticFileHandler.get(self, path)
265 259
266 260 def compute_etag(self):
267 261 return None
268 262
269 263 def validate_absolute_path(self, root, absolute_path):
270 264 """Validate and return the absolute path.
271 265
272 266 Requires tornado 3.1
273 267
274 268 Adding to tornado's own handling, forbids the serving of hidden files.
275 269 """
276 270 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
277 271 abs_root = os.path.abspath(root)
278 272 self.forbid_hidden(abs_root, abs_path)
279 273 return abs_path
280 274
281 275 def forbid_hidden(self, absolute_root, absolute_path):
282 276 """Raise 403 if a file is hidden or contained in a hidden directory.
283 277
284 278 Hidden is determined by either name starting with '.'
285 279 or the UF_HIDDEN flag as reported by stat
286 280 """
287 281 inside_root = absolute_path[len(absolute_root):]
288 282 if any(part.startswith('.') for part in inside_root.split(os.sep)):
289 283 raise web.HTTPError(403)
290 284
291 285 # check UF_HIDDEN on any location up to root
292 286 path = absolute_path
293 287 while path and path.startswith(absolute_root) and path != absolute_root:
294 288 st = os.stat(path)
295 289 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
296 290 raise web.HTTPError(403)
297 291 path = os.path.dirname(path)
298 292
299 293 return absolute_path
300 294
301 295
302 296 def json_errors(method):
303 297 """Decorate methods with this to return GitHub style JSON errors.
304 298
305 299 This should be used on any JSON API on any handler method that can raise HTTPErrors.
306 300
307 301 This will grab the latest HTTPError exception using sys.exc_info
308 302 and then:
309 303
310 304 1. Set the HTTP status code based on the HTTPError
311 305 2. Create and return a JSON body with a message field describing
312 306 the error in a human readable form.
313 307 """
314 308 @functools.wraps(method)
315 309 def wrapper(self, *args, **kwargs):
316 310 try:
317 311 result = method(self, *args, **kwargs)
318 312 except web.HTTPError as e:
319 313 status = e.status_code
320 314 message = e.log_message
321 315 self.set_status(e.status_code)
322 316 self.finish(json.dumps(dict(message=message)))
323 317 except Exception:
324 318 self.log.error("Unhandled error in API request", exc_info=True)
325 319 status = 500
326 320 message = "Unknown server error"
327 321 t, value, tb = sys.exc_info()
328 322 self.set_status(status)
329 323 tb_text = ''.join(traceback.format_exception(t, value, tb))
330 324 reply = dict(message=message, traceback=tb_text)
331 325 self.finish(json.dumps(reply))
332 326 else:
333 327 return result
334 328 return wrapper
335 329
336 330
337 331
338 332 #-----------------------------------------------------------------------------
339 333 # File handler
340 334 #-----------------------------------------------------------------------------
341 335
342 336 # to minimize subclass changes:
343 337 HTTPError = web.HTTPError
344 338
345 339 class FileFindHandler(web.StaticFileHandler):
346 340 """subclass of StaticFileHandler for serving files from a search path"""
347 341
348 342 # cache search results, don't search for files more than once
349 343 _static_paths = {}
350 344
351 345 def initialize(self, path, default_filename=None):
352 346 if isinstance(path, string_types):
353 347 path = [path]
354 348
355 349 self.root = tuple(
356 350 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
357 351 )
358 352 self.default_filename = default_filename
359 353
360 354 def compute_etag(self):
361 355 return None
362 356
363 357 @classmethod
364 358 def get_absolute_path(cls, roots, path):
365 359 """locate a file to serve on our static file search path"""
366 360 with cls._lock:
367 361 if path in cls._static_paths:
368 362 return cls._static_paths[path]
369 363 try:
370 364 abspath = os.path.abspath(filefind(path, roots))
371 365 except IOError:
372 366 # IOError means not found
373 367 return ''
374 368
375 369 cls._static_paths[path] = abspath
376 370 return abspath
377 371
378 372 def validate_absolute_path(self, root, absolute_path):
379 373 """check if the file should be served (raises 404, 403, etc.)"""
380 374 if absolute_path == '':
381 375 raise web.HTTPError(404)
382 376
383 377 for root in self.root:
384 378 if (absolute_path + os.sep).startswith(root):
385 379 break
386 380
387 381 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
388 382
389 383
390 384 class TrailingSlashHandler(web.RequestHandler):
391 385 """Simple redirect handler that strips trailing slashes
392 386
393 387 This should be the first, highest priority handler.
394 388 """
395 389
396 390 SUPPORTED_METHODS = ['GET']
397 391
398 392 def get(self):
399 393 self.redirect(self.request.uri.rstrip('/'))
400 394
401 395 #-----------------------------------------------------------------------------
402 396 # URL pattern fragments for re-use
403 397 #-----------------------------------------------------------------------------
404 398
405 399 path_regex = r"(?P<path>(?:/.*)*)"
406 400 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
407 401 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
408 402
409 403 #-----------------------------------------------------------------------------
410 404 # URL to handler mappings
411 405 #-----------------------------------------------------------------------------
412 406
413 407
414 408 default_handlers = [
415 409 (r".*/", TrailingSlashHandler)
416 410 ]
@@ -1,33 +1,32 b''
1 1 """ fabfile to prepare the notebook """
2 2
3 3 from fabric.api import local,lcd
4 4 from fabric.utils import abort
5 5 import os
6 6
7 7 pjoin = os.path.join
8 8 static_dir = 'static'
9 9 components_dir = os.path.join(static_dir, 'components')
10 10
11 11
12 12 def css(minify=True, verbose=False):
13 13 """generate the css from less files"""
14 14 for name in ('style', 'ipython'):
15 15 source = pjoin('style', "%s.less" % name)
16 16 target = pjoin('style', "%s.min.css" % name)
17 17 _compile_less(source, target, minify, verbose)
18 18
19 19 def _to_bool(b):
20 20 if not b in ['True', 'False', True, False]:
21 21 abort('boolean expected, got: %s' % b)
22 22 return (b in ['True', True])
23 23
24 24 def _compile_less(source, target, minify=True, verbose=False):
25 25 """Compile a less file by source and target relative to static_dir"""
26 26 minify = _to_bool(minify)
27 27 verbose = _to_bool(verbose)
28 28 min_flag = '-x' if minify is True else ''
29 29 ver_flag = '--verbose' if verbose is True else ''
30 lessc = os.path.join('components', 'less.js', 'bin', 'lessc')
31 30 with lcd(static_dir):
32 local('{lessc} {min_flag} {ver_flag} {source} {target}'.format(**locals()))
31 local('lessc {min_flag} {ver_flag} {source} {target}'.format(**locals()))
33 32
@@ -1,849 +1,836 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import io
23 23 import json
24 24 import logging
25 25 import os
26 26 import random
27 27 import select
28 28 import signal
29 29 import socket
30 30 import sys
31 31 import threading
32 32 import time
33 33 import webbrowser
34 34
35 35
36 36 # Third party
37 37 # check for pyzmq 2.1.11
38 38 from IPython.utils.zmqrelated import check_for_zmq
39 39 check_for_zmq('2.1.11', 'IPython.html')
40 40
41 41 from jinja2 import Environment, FileSystemLoader
42 42
43 43 # Install the pyzmq ioloop. This has to be done before anything else from
44 44 # tornado is imported.
45 45 from zmq.eventloop import ioloop
46 46 ioloop.install()
47 47
48 48 # check for tornado 3.1.0
49 49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 50 try:
51 51 import tornado
52 52 except ImportError:
53 53 raise ImportError(msg)
54 54 try:
55 55 version_info = tornado.version_info
56 56 except AttributeError:
57 57 raise ImportError(msg + ", but you have < 1.1.0")
58 58 if version_info < (3,1,0):
59 59 raise ImportError(msg + ", but you have %s" % tornado.version)
60 60
61 61 from tornado import httpserver
62 62 from tornado import web
63 63
64 64 # Our own libraries
65 65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 66 from .base.handlers import Template404
67 67
68 68 from .services.kernels.kernelmanager import MappingKernelManager
69 69 from .services.notebooks.nbmanager import NotebookManager
70 70 from .services.notebooks.filenbmanager import FileNotebookManager
71 71 from .services.clusters.clustermanager import ClusterManager
72 72 from .services.sessions.sessionmanager import SessionManager
73 73
74 74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75 75
76 76 from IPython.config.application import catch_config_error, boolean_flag
77 77 from IPython.core.application import BaseIPythonApplication
78 78 from IPython.core.profiledir import ProfileDir
79 79 from IPython.consoleapp import IPythonConsoleApp
80 80 from IPython.kernel import swallow_argv
81 81 from IPython.kernel.zmq.session import default_secure
82 82 from IPython.kernel.zmq.kernelapp import (
83 83 kernel_flags,
84 84 kernel_aliases,
85 85 )
86 86 from IPython.utils.importstring import import_item
87 87 from IPython.utils.localinterfaces import localhost
88 88 from IPython.utils import submodule
89 89 from IPython.utils.traitlets import (
90 90 Dict, Unicode, Integer, List, Bool, Bytes,
91 91 DottedObjectName
92 92 )
93 93 from IPython.utils import py3compat
94 94 from IPython.utils.path import filefind, get_ipython_dir
95 95
96 96 from .utils import url_path_join
97 97
98 98 #-----------------------------------------------------------------------------
99 99 # Module globals
100 100 #-----------------------------------------------------------------------------
101 101
102 102 _examples = """
103 103 ipython notebook # start the notebook
104 104 ipython notebook --profile=sympy # use the sympy profile
105 105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 106 """
107 107
108 108 #-----------------------------------------------------------------------------
109 109 # Helper functions
110 110 #-----------------------------------------------------------------------------
111 111
112 112 def random_ports(port, n):
113 113 """Generate a list of n random ports near the given port.
114 114
115 115 The first 5 ports will be sequential, and the remaining n-5 will be
116 116 randomly selected in the range [port-2*n, port+2*n].
117 117 """
118 118 for i in range(min(5, n)):
119 119 yield port + i
120 120 for i in range(n-5):
121 121 yield max(1, port + random.randint(-2*n, 2*n))
122 122
123 123 def load_handlers(name):
124 124 """Load the (URL pattern, handler) tuples for each component."""
125 125 name = 'IPython.html.' + name
126 126 mod = __import__(name, fromlist=['default_handlers'])
127 127 return mod.default_handlers
128 128
129 129 #-----------------------------------------------------------------------------
130 130 # The Tornado web application
131 131 #-----------------------------------------------------------------------------
132 132
133 133 class NotebookWebApplication(web.Application):
134 134
135 135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 136 cluster_manager, session_manager, log, base_project_url,
137 137 settings_overrides):
138 138
139 139 settings = self.init_settings(
140 140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 141 session_manager, log, base_project_url, settings_overrides)
142 142 handlers = self.init_handlers(settings)
143 143
144 144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 145
146 146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 147 cluster_manager, session_manager, log, base_project_url,
148 148 settings_overrides):
149 149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 150 # base_project_url will always be unicode, which will in turn
151 151 # make the patterns unicode, and ultimately result in unicode
152 152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 153 # This enforces that base_project_url be ascii in that situation.
154 154 #
155 155 # Note that the URLs these patterns check against are escaped,
156 156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 157 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
158 158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 159 settings = dict(
160 160 # basics
161 161 base_project_url=base_project_url,
162 162 base_kernel_url=ipython_app.base_kernel_url,
163 163 template_path=template_path,
164 164 static_path=ipython_app.static_file_path,
165 165 static_handler_class = FileFindHandler,
166 166 static_url_prefix = url_path_join(base_project_url,'/static/'),
167 167
168 168 # authentication
169 169 cookie_secret=ipython_app.cookie_secret,
170 170 login_url=url_path_join(base_project_url,'/login'),
171 171 password=ipython_app.password,
172 172
173 173 # managers
174 174 kernel_manager=kernel_manager,
175 175 notebook_manager=notebook_manager,
176 176 cluster_manager=cluster_manager,
177 177 session_manager=session_manager,
178 178
179 179 # IPython stuff
180 180 nbextensions_path = ipython_app.nbextensions_path,
181 181 mathjax_url=ipython_app.mathjax_url,
182 182 config=ipython_app.config,
183 use_less=ipython_app.use_less,
184 183 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
185 184 )
186 185
187 186 # allow custom overrides for the tornado web app.
188 187 settings.update(settings_overrides)
189 188 return settings
190 189
191 190 def init_handlers(self, settings):
192 191 # Load the (URL pattern, handler) tuples for each component.
193 192 handlers = []
194 193 handlers.extend(load_handlers('base.handlers'))
195 194 handlers.extend(load_handlers('tree.handlers'))
196 195 handlers.extend(load_handlers('auth.login'))
197 196 handlers.extend(load_handlers('auth.logout'))
198 197 handlers.extend(load_handlers('notebook.handlers'))
199 198 handlers.extend(load_handlers('nbconvert.handlers'))
200 199 handlers.extend(load_handlers('services.kernels.handlers'))
201 200 handlers.extend(load_handlers('services.notebooks.handlers'))
202 201 handlers.extend(load_handlers('services.clusters.handlers'))
203 202 handlers.extend(load_handlers('services.sessions.handlers'))
204 203 handlers.extend(load_handlers('services.nbconvert.handlers'))
205 204 handlers.extend([
206 205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
207 206 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 207 ])
209 208 # prepend base_project_url onto the patterns that we match
210 209 new_handlers = []
211 210 for handler in handlers:
212 211 pattern = url_path_join(settings['base_project_url'], handler[0])
213 212 new_handler = tuple([pattern] + list(handler[1:]))
214 213 new_handlers.append(new_handler)
215 214 # add 404 on the end, which will catch everything that falls through
216 215 new_handlers.append((r'(.*)', Template404))
217 216 return new_handlers
218 217
219 218
220 219 class NbserverListApp(BaseIPythonApplication):
221 220
222 221 description="List currently running notebook servers in this profile."
223 222
224 223 flags = dict(
225 224 json=({'NbserverListApp': {'json': True}},
226 225 "Produce machine-readable JSON output."),
227 226 )
228 227
229 228 json = Bool(False, config=True,
230 229 help="If True, each line of output will be a JSON object with the "
231 230 "details from the server info file.")
232 231
233 232 def start(self):
234 233 if not self.json:
235 234 print("Currently running servers:")
236 235 for serverinfo in list_running_servers(self.profile):
237 236 if self.json:
238 237 print(json.dumps(serverinfo))
239 238 else:
240 239 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
241 240
242 241 #-----------------------------------------------------------------------------
243 242 # Aliases and Flags
244 243 #-----------------------------------------------------------------------------
245 244
246 245 flags = dict(kernel_flags)
247 246 flags['no-browser']=(
248 247 {'NotebookApp' : {'open_browser' : False}},
249 248 "Don't open the notebook in a browser after startup."
250 249 )
251 250 flags['no-mathjax']=(
252 251 {'NotebookApp' : {'enable_mathjax' : False}},
253 252 """Disable MathJax
254 253
255 254 MathJax is the javascript library IPython uses to render math/LaTeX. It is
256 255 very large, so you may want to disable it if you have a slow internet
257 256 connection, or for offline use of the notebook.
258 257
259 258 When disabled, equations etc. will appear as their untransformed TeX source.
260 259 """
261 260 )
262 261
263 262 # Add notebook manager flags
264 263 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
265 264 'Auto-save a .py script everytime the .ipynb notebook is saved',
266 265 'Do not auto-save .py scripts for every notebook'))
267 266
268 267 # the flags that are specific to the frontend
269 268 # these must be scrubbed before being passed to the kernel,
270 269 # or it will raise an error on unrecognized flags
271 270 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
272 271
273 272 aliases = dict(kernel_aliases)
274 273
275 274 aliases.update({
276 275 'ip': 'NotebookApp.ip',
277 276 'port': 'NotebookApp.port',
278 277 'port-retries': 'NotebookApp.port_retries',
279 278 'transport': 'KernelManager.transport',
280 279 'keyfile': 'NotebookApp.keyfile',
281 280 'certfile': 'NotebookApp.certfile',
282 281 'notebook-dir': 'NotebookManager.notebook_dir',
283 282 'browser': 'NotebookApp.browser',
284 283 })
285 284
286 285 # remove ipkernel flags that are singletons, and don't make sense in
287 286 # multi-kernel evironment:
288 287 aliases.pop('f', None)
289 288
290 289 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
291 290 u'notebook-dir', u'profile', u'profile-dir']
292 291
293 292 #-----------------------------------------------------------------------------
294 293 # NotebookApp
295 294 #-----------------------------------------------------------------------------
296 295
297 296 class NotebookApp(BaseIPythonApplication):
298 297
299 298 name = 'ipython-notebook'
300 299
301 300 description = """
302 301 The IPython HTML Notebook.
303 302
304 303 This launches a Tornado based HTML Notebook Server that serves up an
305 304 HTML5/Javascript Notebook client.
306 305 """
307 306 examples = _examples
308 307
309 308 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
310 309 FileNotebookManager]
311 310 flags = Dict(flags)
312 311 aliases = Dict(aliases)
313 312
314 313 subcommands = dict(
315 314 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
316 315 )
317 316
318 317 kernel_argv = List(Unicode)
319 318
320 319 def _log_level_default(self):
321 320 return logging.INFO
322 321
323 322 def _log_format_default(self):
324 323 """override default log format to include time"""
325 324 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
326 325
327 326 # create requested profiles by default, if they don't exist:
328 327 auto_create = Bool(True)
329 328
330 329 # file to be opened in the notebook server
331 330 file_to_run = Unicode('')
332 331
333 332 # Network related information.
334 333
335 334 ip = Unicode(config=True,
336 335 help="The IP address the notebook server will listen on."
337 336 )
338 337 def _ip_default(self):
339 338 return localhost()
340 339
341 340 def _ip_changed(self, name, old, new):
342 341 if new == u'*': self.ip = u''
343 342
344 343 port = Integer(8888, config=True,
345 344 help="The port the notebook server will listen on."
346 345 )
347 346 port_retries = Integer(50, config=True,
348 347 help="The number of additional ports to try if the specified port is not available."
349 348 )
350 349
351 350 certfile = Unicode(u'', config=True,
352 351 help="""The full path to an SSL/TLS certificate file."""
353 352 )
354 353
355 354 keyfile = Unicode(u'', config=True,
356 355 help="""The full path to a private key file for usage with SSL/TLS."""
357 356 )
358 357
359 358 cookie_secret = Bytes(b'', config=True,
360 359 help="""The random bytes used to secure cookies.
361 360 By default this is a new random number every time you start the Notebook.
362 361 Set it to a value in a config file to enable logins to persist across server sessions.
363 362
364 363 Note: Cookie secrets should be kept private, do not share config files with
365 364 cookie_secret stored in plaintext (you can read the value from a file).
366 365 """
367 366 )
368 367 def _cookie_secret_default(self):
369 368 return os.urandom(1024)
370 369
371 370 password = Unicode(u'', config=True,
372 371 help="""Hashed password to use for web authentication.
373 372
374 373 To generate, type in a python/IPython shell:
375 374
376 375 from IPython.lib import passwd; passwd()
377 376
378 377 The string should be of the form type:salt:hashed-password.
379 378 """
380 379 )
381 380
382 381 open_browser = Bool(True, config=True,
383 382 help="""Whether to open in a browser after starting.
384 383 The specific browser used is platform dependent and
385 384 determined by the python standard library `webbrowser`
386 385 module, unless it is overridden using the --browser
387 386 (NotebookApp.browser) configuration option.
388 387 """)
389 388
390 389 browser = Unicode(u'', config=True,
391 390 help="""Specify what command to use to invoke a web
392 391 browser when opening the notebook. If not specified, the
393 392 default browser will be determined by the `webbrowser`
394 393 standard library module, which allows setting of the
395 394 BROWSER environment variable to override it.
396 395 """)
397 396
398 use_less = Bool(False, config=True,
399 help="""Wether to use Browser Side less-css parsing
400 instead of compiled css version in templates that allows
401 it. This is mainly convenient when working on the less
402 file to avoid a build step, or if user want to overwrite
403 some of the less variables without having to recompile
404 everything.
405
406 You will need to install the less.js component in the static directory
407 either in the source tree or in your profile folder.
408 """)
409
410 397 webapp_settings = Dict(config=True,
411 398 help="Supply overrides for the tornado.web.Application that the "
412 399 "IPython notebook uses.")
413 400
414 401 enable_mathjax = Bool(True, config=True,
415 402 help="""Whether to enable MathJax for typesetting math/TeX
416 403
417 404 MathJax is the javascript library IPython uses to render math/LaTeX. It is
418 405 very large, so you may want to disable it if you have a slow internet
419 406 connection, or for offline use of the notebook.
420 407
421 408 When disabled, equations etc. will appear as their untransformed TeX source.
422 409 """
423 410 )
424 411 def _enable_mathjax_changed(self, name, old, new):
425 412 """set mathjax url to empty if mathjax is disabled"""
426 413 if not new:
427 414 self.mathjax_url = u''
428 415
429 416 base_project_url = Unicode('/', config=True,
430 417 help='''The base URL for the notebook server.
431 418
432 419 Leading and trailing slashes can be omitted,
433 420 and will automatically be added.
434 421 ''')
435 422 def _base_project_url_changed(self, name, old, new):
436 423 if not new.startswith('/'):
437 424 self.base_project_url = '/'+new
438 425 elif not new.endswith('/'):
439 426 self.base_project_url = new+'/'
440 427
441 428 base_kernel_url = Unicode('/', config=True,
442 429 help='''The base URL for the kernel server
443 430
444 431 Leading and trailing slashes can be omitted,
445 432 and will automatically be added.
446 433 ''')
447 434 def _base_kernel_url_changed(self, name, old, new):
448 435 if not new.startswith('/'):
449 436 self.base_kernel_url = '/'+new
450 437 elif not new.endswith('/'):
451 438 self.base_kernel_url = new+'/'
452 439
453 440 websocket_url = Unicode("", config=True,
454 441 help="""The base URL for the websocket server,
455 442 if it differs from the HTTP server (hint: it almost certainly doesn't).
456 443
457 444 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
458 445 """
459 446 )
460 447
461 448 extra_static_paths = List(Unicode, config=True,
462 449 help="""Extra paths to search for serving static files.
463 450
464 451 This allows adding javascript/css to be available from the notebook server machine,
465 452 or overriding individual files in the IPython"""
466 453 )
467 454 def _extra_static_paths_default(self):
468 455 return [os.path.join(self.profile_dir.location, 'static')]
469 456
470 457 @property
471 458 def static_file_path(self):
472 459 """return extra paths + the default location"""
473 460 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
474 461
475 462 nbextensions_path = List(Unicode, config=True,
476 463 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
477 464 )
478 465 def _nbextensions_path_default(self):
479 466 return [os.path.join(get_ipython_dir(), 'nbextensions')]
480 467
481 468 mathjax_url = Unicode("", config=True,
482 469 help="""The url for MathJax.js."""
483 470 )
484 471 def _mathjax_url_default(self):
485 472 if not self.enable_mathjax:
486 473 return u''
487 474 static_url_prefix = self.webapp_settings.get("static_url_prefix",
488 475 url_path_join(self.base_project_url, "static")
489 476 )
490 477
491 478 # try local mathjax, either in nbextensions/mathjax or static/mathjax
492 479 for (url_prefix, search_path) in [
493 480 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
494 481 (static_url_prefix, self.static_file_path),
495 482 ]:
496 483 self.log.debug("searching for local mathjax in %s", search_path)
497 484 try:
498 485 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
499 486 except IOError:
500 487 continue
501 488 else:
502 489 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
503 490 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
504 491 return url
505 492
506 493 # no local mathjax, serve from CDN
507 494 if self.certfile:
508 495 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
509 496 host = u"https://c328740.ssl.cf1.rackcdn.com"
510 497 else:
511 498 host = u"http://cdn.mathjax.org"
512 499
513 500 url = host + u"/mathjax/latest/MathJax.js"
514 501 self.log.info("Using MathJax from CDN: %s", url)
515 502 return url
516 503
517 504 def _mathjax_url_changed(self, name, old, new):
518 505 if new and not self.enable_mathjax:
519 506 # enable_mathjax=False overrides mathjax_url
520 507 self.mathjax_url = u''
521 508 else:
522 509 self.log.info("Using MathJax: %s", new)
523 510
524 511 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
525 512 config=True,
526 513 help='The notebook manager class to use.')
527 514
528 515 trust_xheaders = Bool(False, config=True,
529 516 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
530 517 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
531 518 )
532 519
533 520 info_file = Unicode()
534 521
535 522 def _info_file_default(self):
536 523 info_file = "nbserver-%s.json"%os.getpid()
537 524 return os.path.join(self.profile_dir.security_dir, info_file)
538 525
539 526 def parse_command_line(self, argv=None):
540 527 super(NotebookApp, self).parse_command_line(argv)
541 528
542 529 if self.extra_args:
543 530 arg0 = self.extra_args[0]
544 531 f = os.path.abspath(arg0)
545 532 self.argv.remove(arg0)
546 533 if not os.path.exists(f):
547 534 self.log.critical("No such file or directory: %s", f)
548 535 self.exit(1)
549 536 if os.path.isdir(f):
550 537 self.config.FileNotebookManager.notebook_dir = f
551 538 elif os.path.isfile(f):
552 539 self.file_to_run = f
553 540
554 541 def init_kernel_argv(self):
555 542 """construct the kernel arguments"""
556 543 # Scrub frontend-specific flags
557 544 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
558 545 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
559 546 self.log.warn('\n '.join([
560 547 "Starting all kernels in pylab mode is not recommended,",
561 548 "and will be disabled in a future release.",
562 549 "Please use the %matplotlib magic to enable matplotlib instead.",
563 550 "pylab implies many imports, which can have confusing side effects",
564 551 "and harm the reproducibility of your notebooks.",
565 552 ]))
566 553 # Kernel should inherit default config file from frontend
567 554 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
568 555 # Kernel should get *absolute* path to profile directory
569 556 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
570 557
571 558 def init_configurables(self):
572 559 # force Session default to be secure
573 560 default_secure(self.config)
574 561 self.kernel_manager = MappingKernelManager(
575 562 parent=self, log=self.log, kernel_argv=self.kernel_argv,
576 563 connection_dir = self.profile_dir.security_dir,
577 564 )
578 565 kls = import_item(self.notebook_manager_class)
579 566 self.notebook_manager = kls(parent=self, log=self.log)
580 567 self.session_manager = SessionManager(parent=self, log=self.log)
581 568 self.cluster_manager = ClusterManager(parent=self, log=self.log)
582 569 self.cluster_manager.update_profiles()
583 570
584 571 def init_logging(self):
585 572 # This prevents double log messages because tornado use a root logger that
586 573 # self.log is a child of. The logging module dipatches log messages to a log
587 574 # and all of its ancenstors until propagate is set to False.
588 575 self.log.propagate = False
589 576
590 577 # hook up tornado 3's loggers to our app handlers
591 578 for name in ('access', 'application', 'general'):
592 579 logger = logging.getLogger('tornado.%s' % name)
593 580 logger.parent = self.log
594 581 logger.setLevel(self.log.level)
595 582
596 583 def init_webapp(self):
597 584 """initialize tornado webapp and httpserver"""
598 585 self.web_app = NotebookWebApplication(
599 586 self, self.kernel_manager, self.notebook_manager,
600 587 self.cluster_manager, self.session_manager,
601 588 self.log, self.base_project_url, self.webapp_settings
602 589 )
603 590 if self.certfile:
604 591 ssl_options = dict(certfile=self.certfile)
605 592 if self.keyfile:
606 593 ssl_options['keyfile'] = self.keyfile
607 594 else:
608 595 ssl_options = None
609 596 self.web_app.password = self.password
610 597 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
611 598 xheaders=self.trust_xheaders)
612 599 if not self.ip:
613 600 warning = "WARNING: The notebook server is listening on all IP addresses"
614 601 if ssl_options is None:
615 602 self.log.critical(warning + " and not using encryption. This "
616 603 "is not recommended.")
617 604 if not self.password:
618 605 self.log.critical(warning + " and not using authentication. "
619 606 "This is highly insecure and not recommended.")
620 607 success = None
621 608 for port in random_ports(self.port, self.port_retries+1):
622 609 try:
623 610 self.http_server.listen(port, self.ip)
624 611 except socket.error as e:
625 612 if e.errno == errno.EADDRINUSE:
626 613 self.log.info('The port %i is already in use, trying another random port.' % port)
627 614 continue
628 615 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
629 616 self.log.warn("Permission to listen on port %i denied" % port)
630 617 continue
631 618 else:
632 619 raise
633 620 else:
634 621 self.port = port
635 622 success = True
636 623 break
637 624 if not success:
638 625 self.log.critical('ERROR: the notebook server could not be started because '
639 626 'no available port could be found.')
640 627 self.exit(1)
641 628
642 629 @property
643 630 def display_url(self):
644 631 ip = self.ip if self.ip else '[all ip addresses on your system]'
645 632 return self._url(ip)
646 633
647 634 @property
648 635 def connection_url(self):
649 636 ip = self.ip if self.ip else localhost()
650 637 return self._url(ip)
651 638
652 639 def _url(self, ip):
653 640 proto = 'https' if self.certfile else 'http'
654 641 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
655 642
656 643 def init_signal(self):
657 644 if not sys.platform.startswith('win'):
658 645 signal.signal(signal.SIGINT, self._handle_sigint)
659 646 signal.signal(signal.SIGTERM, self._signal_stop)
660 647 if hasattr(signal, 'SIGUSR1'):
661 648 # Windows doesn't support SIGUSR1
662 649 signal.signal(signal.SIGUSR1, self._signal_info)
663 650 if hasattr(signal, 'SIGINFO'):
664 651 # only on BSD-based systems
665 652 signal.signal(signal.SIGINFO, self._signal_info)
666 653
667 654 def _handle_sigint(self, sig, frame):
668 655 """SIGINT handler spawns confirmation dialog"""
669 656 # register more forceful signal handler for ^C^C case
670 657 signal.signal(signal.SIGINT, self._signal_stop)
671 658 # request confirmation dialog in bg thread, to avoid
672 659 # blocking the App
673 660 thread = threading.Thread(target=self._confirm_exit)
674 661 thread.daemon = True
675 662 thread.start()
676 663
677 664 def _restore_sigint_handler(self):
678 665 """callback for restoring original SIGINT handler"""
679 666 signal.signal(signal.SIGINT, self._handle_sigint)
680 667
681 668 def _confirm_exit(self):
682 669 """confirm shutdown on ^C
683 670
684 671 A second ^C, or answering 'y' within 5s will cause shutdown,
685 672 otherwise original SIGINT handler will be restored.
686 673
687 674 This doesn't work on Windows.
688 675 """
689 676 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
690 677 time.sleep(0.1)
691 678 info = self.log.info
692 679 info('interrupted')
693 680 print(self.notebook_info())
694 681 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
695 682 sys.stdout.flush()
696 683 r,w,x = select.select([sys.stdin], [], [], 5)
697 684 if r:
698 685 line = sys.stdin.readline()
699 686 if line.lower().startswith('y'):
700 687 self.log.critical("Shutdown confirmed")
701 688 ioloop.IOLoop.instance().stop()
702 689 return
703 690 else:
704 691 print("No answer for 5s:", end=' ')
705 692 print("resuming operation...")
706 693 # no answer, or answer is no:
707 694 # set it back to original SIGINT handler
708 695 # use IOLoop.add_callback because signal.signal must be called
709 696 # from main thread
710 697 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
711 698
712 699 def _signal_stop(self, sig, frame):
713 700 self.log.critical("received signal %s, stopping", sig)
714 701 ioloop.IOLoop.instance().stop()
715 702
716 703 def _signal_info(self, sig, frame):
717 704 print(self.notebook_info())
718 705
719 706 def init_components(self):
720 707 """Check the components submodule, and warn if it's unclean"""
721 708 status = submodule.check_submodule_status()
722 709 if status == 'missing':
723 710 self.log.warn("components submodule missing, running `git submodule update`")
724 711 submodule.update_submodules(submodule.ipython_parent())
725 712 elif status == 'unclean':
726 713 self.log.warn("components submodule unclean, you may see 404s on static/components")
727 714 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
728 715
729 716 @catch_config_error
730 717 def initialize(self, argv=None):
731 718 super(NotebookApp, self).initialize(argv)
732 719 self.init_logging()
733 720 self.init_kernel_argv()
734 721 self.init_configurables()
735 722 self.init_components()
736 723 self.init_webapp()
737 724 self.init_signal()
738 725
739 726 def cleanup_kernels(self):
740 727 """Shutdown all kernels.
741 728
742 729 The kernels will shutdown themselves when this process no longer exists,
743 730 but explicit shutdown allows the KernelManagers to cleanup the connection files.
744 731 """
745 732 self.log.info('Shutting down kernels')
746 733 self.kernel_manager.shutdown_all()
747 734
748 735 def notebook_info(self):
749 736 "Return the current working directory and the server url information"
750 737 info = self.notebook_manager.info_string() + "\n"
751 738 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
752 739 return info + "The IPython Notebook is running at: %s" % self.display_url
753 740
754 741 def server_info(self):
755 742 """Return a JSONable dict of information about this server."""
756 743 return {'url': self.connection_url,
757 744 'hostname': self.ip if self.ip else 'localhost',
758 745 'port': self.port,
759 746 'secure': bool(self.certfile),
760 747 'base_project_url': self.base_project_url,
761 748 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
762 749 }
763 750
764 751 def write_server_info_file(self):
765 752 """Write the result of server_info() to the JSON file info_file."""
766 753 with open(self.info_file, 'w') as f:
767 754 json.dump(self.server_info(), f, indent=2)
768 755
769 756 def remove_server_info_file(self):
770 757 """Remove the nbserver-<pid>.json file created for this server.
771 758
772 759 Ignores the error raised when the file has already been removed.
773 760 """
774 761 try:
775 762 os.unlink(self.info_file)
776 763 except OSError as e:
777 764 if e.errno != errno.ENOENT:
778 765 raise
779 766
780 767 def start(self):
781 768 """ Start the IPython Notebook server app, after initialization
782 769
783 770 This method takes no arguments so all configuration and initialization
784 771 must be done prior to calling this method."""
785 772 if self.subapp is not None:
786 773 return self.subapp.start()
787 774
788 775 info = self.log.info
789 776 for line in self.notebook_info().split("\n"):
790 777 info(line)
791 778 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
792 779
793 780 self.write_server_info_file()
794 781
795 782 if self.open_browser or self.file_to_run:
796 783 try:
797 784 browser = webbrowser.get(self.browser or None)
798 785 except webbrowser.Error as e:
799 786 self.log.warn('No web browser found: %s.' % e)
800 787 browser = None
801 788
802 789 f = self.file_to_run
803 790 if f:
804 791 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
805 792 if f.startswith(nbdir):
806 793 f = f[len(nbdir):]
807 794 else:
808 795 self.log.warn(
809 796 "Probably won't be able to open notebook %s "
810 797 "because it is not in notebook_dir %s",
811 798 f, nbdir,
812 799 )
813 800
814 801 if os.path.isfile(self.file_to_run):
815 802 url = url_path_join('notebooks', f)
816 803 else:
817 804 url = url_path_join('tree', f)
818 805 if browser:
819 806 b = lambda : browser.open("%s%s" % (self.connection_url, url),
820 807 new=2)
821 808 threading.Thread(target=b).start()
822 809 try:
823 810 ioloop.IOLoop.instance().start()
824 811 except KeyboardInterrupt:
825 812 info("Interrupted...")
826 813 finally:
827 814 self.cleanup_kernels()
828 815 self.remove_server_info_file()
829 816
830 817
831 818 def list_running_servers(profile='default'):
832 819 """Iterate over the server info files of running notebook servers.
833 820
834 821 Given a profile name, find nbserver-* files in the security directory of
835 822 that profile, and yield dicts of their information, each one pertaining to
836 823 a currently running notebook server instance.
837 824 """
838 825 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
839 826 for file in os.listdir(pd.security_dir):
840 827 if file.startswith('nbserver-'):
841 828 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
842 829 yield json.load(f)
843 830
844 831 #-----------------------------------------------------------------------------
845 832 # Main entry point
846 833 #-----------------------------------------------------------------------------
847 834
848 835 launch_new_instance = NotebookApp.launch_instance
849 836
@@ -1,1 +1,1 b''
1 Subproject commit 1977b852048ecb05f66d3b8980221080c5decc49
1 Subproject commit 0972b5683e1300f378537aa8eb6350a55070cbb9
@@ -1,94 +1,85 b''
1 1
2 2
3 3 <!DOCTYPE HTML>
4 4 <html>
5 5
6 6 <head>
7 7 <meta charset="utf-8">
8 8
9 9 <title>{% block title %}IPython Notebook{% endblock %}</title>
10 10 <link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">
11 11 <meta http-equiv="X-UA-Compatible" content="chrome=1">
12 12 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
13 13 <meta name="viewport" content="width=device-width, initial-scale=1.0">
14 14
15 15 {% block stylesheet %}
16 {% block lesscss %}
17 {% if use_less %}
18 <link rel="stylesheet/less" href="{{ static_url("style/style.less") }}" type="text/css" />
19 {% else %}
20 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
21 {% endif %}
22 {% endblock %}
16 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
23 17 {% endblock %}
24 18 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
25 19 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
26 20 <script>
27 21 require.config({
28 22 baseUrl: '{{static_url("")}}',
29 23 paths: {
30 24 nbextensions : '{{ base_project_url }}nbextensions'
31 25 }
32 26 });
33 27 </script>
34 28
35 29 {% block meta %}
36 30 {% endblock %}
37 31
38 32 </head>
39 33
40 34 <body {% block params %}{% endblock %}>
41 35
42 36 <noscript>
43 37 <div id='noscript'>
44 38 IPython Notebook requires JavaScript.<br>
45 39 Please enable it to proceed.
46 40 </div>
47 41 </noscript>
48 42
49 43 <div id="header" class="navbar navbar-static-top">
50 44 <div class="navbar-inner navbar-nobg">
51 45 <div class="container">
52 46 <div id="ipython_notebook" class="nav brand pull-left"><a href="{{base_project_url}}tree/{{notebook_path}}" alt='dashboard'><img src='{{static_url("base/images/ipynblogo.png") }}' alt='IPython Notebook'/></a></div>
53 47
54 48 {% block login_widget %}
55 49
56 50 <span id="login_widget">
57 51 {% if logged_in %}
58 52 <button id="logout">Logout</button>
59 53 {% elif login_available and not logged_in %}
60 54 <button id="login">Login</button>
61 55 {% endif %}
62 56 </span>
63 57
64 58 {% endblock %}
65 59
66 60 {% block header %}
67 61 {% endblock %}
68 62 </div>
69 63 </div>
70 64 </div>
71 65
72 66 <div id="site">
73 67 {% block site %}
74 68 {% endblock %}
75 69 </div>
76 70
77 71 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
78 72 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
79 73 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
80 74 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
81 75 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
82 76 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
83 77
84 78 {% block script %}
85 {% if use_less %}
86 <script src="{{ static_url("components/less.js/dist/less-1.3.3.min.js") }}" charset="utf-8"></script>
87 {% endif %}
88 79 {% endblock %}
89 80
90 81 <script src="{{static_url("custom/custom.js") }}" type="text/javascript" charset="utf-8"></script>
91 82
92 83 </body>
93 84
94 85 </html>
@@ -1,36 +1,36 b''
1 1 //
2 2 // Test that a Markdown cell is rendered to HTML.
3 3 //
4 4 casper.notebook_test(function () {
5 5 // Test JavaScript models.
6 6 var output = this.evaluate(function () {
7 7 IPython.notebook.to_markdown();
8 8 var cell = IPython.notebook.get_selected_cell();
9 9 cell.set_text('# Foo');
10 10 cell.render();
11 11 return cell.get_rendered();
12 12 });
13 this.test.assertEquals(output, '<h1>Foo</h1>', 'Markdown JS API works.');
13 this.test.assertEquals(output, '<h1 id=\"foo\">Foo</h1>', 'Markdown JS API works.');
14 14
15 15 // Test menubar entries.
16 16 output = this.evaluate(function () {
17 17 $('#to_code').mouseenter().click();
18 18 $('#to_markdown').mouseenter().click();
19 19 var cell = IPython.notebook.get_selected_cell();
20 20 cell.set_text('# Foo');
21 21 $('#run_cell').mouseenter().click();
22 22 return cell.get_rendered();
23 23 });
24 this.test.assertEquals(output, '<h1>Foo</h1>', 'Markdown menubar items work.');
24 this.test.assertEquals(output, '<h1 id=\"foo\">Foo</h1>', 'Markdown menubar items work.');
25 25
26 26 // Test toolbar buttons.
27 27 output = this.evaluate(function () {
28 28 $('#cell_type').val('code').change();
29 29 $('#cell_type').val('markdown').change();
30 30 var cell = IPython.notebook.get_selected_cell();
31 31 cell.set_text('# Foo');
32 32 $('#run_b').click();
33 33 return cell.get_rendered();
34 34 });
35 this.test.assertEquals(output, '<h1>Foo</h1>', 'Markdown toolbar items work.');
35 this.test.assertEquals(output, '<h1 id=\"foo\">Foo</h1>', 'Markdown toolbar items work.');
36 36 });
General Comments 0
You need to be logged in to leave comments. Login now