##// END OF EJS Templates
DEV: Allow supplying jinja vars from python config.
Scott Sanderson -
Show More
@@ -1,522 +1,528 b''
1 """Base Tornado handlers for the notebook server."""
1 """Base Tornado handlers for the notebook server."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 from tornado import gen
22 from tornado import gen
23 from tornado.log import app_log
23 from tornado.log import app_log
24
24
25
25
26 import IPython
26 import IPython
27 from IPython.utils.sysinfo import get_sys_info
27 from IPython.utils.sysinfo import get_sys_info
28
28
29 from IPython.config import Application
29 from IPython.config import Application
30 from IPython.utils.path import filefind
30 from IPython.utils.path import filefind
31 from IPython.utils.py3compat import string_types
31 from IPython.utils.py3compat import string_types
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
32 from IPython.html.utils import is_hidden, url_path_join, url_escape
33
33
34 from IPython.html.services.security import csp_report_uri
34 from IPython.html.services.security import csp_report_uri
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Top-level handlers
37 # Top-level handlers
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40
40
41 sys_info = json.dumps(get_sys_info())
41 sys_info = json.dumps(get_sys_info())
42
42
43 class AuthenticatedHandler(web.RequestHandler):
43 class AuthenticatedHandler(web.RequestHandler):
44 """A RequestHandler with an authenticated user."""
44 """A RequestHandler with an authenticated user."""
45
45
46 def set_default_headers(self):
46 def set_default_headers(self):
47 headers = self.settings.get('headers', {})
47 headers = self.settings.get('headers', {})
48
48
49 if "Content-Security-Policy" not in headers:
49 if "Content-Security-Policy" not in headers:
50 headers["Content-Security-Policy"] = (
50 headers["Content-Security-Policy"] = (
51 "frame-ancestors 'self'; "
51 "frame-ancestors 'self'; "
52 # Make sure the report-uri is relative to the base_url
52 # Make sure the report-uri is relative to the base_url
53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
54 )
54 )
55
55
56 # Allow for overriding headers
56 # Allow for overriding headers
57 for header_name,value in headers.items() :
57 for header_name,value in headers.items() :
58 try:
58 try:
59 self.set_header(header_name, value)
59 self.set_header(header_name, value)
60 except Exception as e:
60 except Exception as e:
61 # tornado raise Exception (not a subclass)
61 # tornado raise Exception (not a subclass)
62 # if method is unsupported (websocket and Access-Control-Allow-Origin
62 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 # for example, so just ignore)
63 # for example, so just ignore)
64 self.log.debug(e)
64 self.log.debug(e)
65
65
66 def clear_login_cookie(self):
66 def clear_login_cookie(self):
67 self.clear_cookie(self.cookie_name)
67 self.clear_cookie(self.cookie_name)
68
68
69 def get_current_user(self):
69 def get_current_user(self):
70 if self.login_handler is None:
70 if self.login_handler is None:
71 return 'anonymous'
71 return 'anonymous'
72 return self.login_handler.get_user(self)
72 return self.login_handler.get_user(self)
73
73
74 @property
74 @property
75 def cookie_name(self):
75 def cookie_name(self):
76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
77 self.request.host
77 self.request.host
78 ))
78 ))
79 return self.settings.get('cookie_name', default_cookie_name)
79 return self.settings.get('cookie_name', default_cookie_name)
80
80
81 @property
81 @property
82 def logged_in(self):
82 def logged_in(self):
83 """Is a user currently logged in?"""
83 """Is a user currently logged in?"""
84 user = self.get_current_user()
84 user = self.get_current_user()
85 return (user and not user == 'anonymous')
85 return (user and not user == 'anonymous')
86
86
87 @property
87 @property
88 def login_handler(self):
88 def login_handler(self):
89 """Return the login handler for this application, if any."""
89 """Return the login handler for this application, if any."""
90 return self.settings.get('login_handler_class', None)
90 return self.settings.get('login_handler_class', None)
91
91
92 @property
92 @property
93 def login_available(self):
93 def login_available(self):
94 """May a user proceed to log in?
94 """May a user proceed to log in?
95
95
96 This returns True if login capability is available, irrespective of
96 This returns True if login capability is available, irrespective of
97 whether the user is already logged in or not.
97 whether the user is already logged in or not.
98
98
99 """
99 """
100 if self.login_handler is None:
100 if self.login_handler is None:
101 return False
101 return False
102 return bool(self.login_handler.login_available(self.settings))
102 return bool(self.login_handler.login_available(self.settings))
103
103
104
104
105 class IPythonHandler(AuthenticatedHandler):
105 class IPythonHandler(AuthenticatedHandler):
106 """IPython-specific extensions to authenticated handling
106 """IPython-specific extensions to authenticated handling
107
107
108 Mostly property shortcuts to IPython-specific settings.
108 Mostly property shortcuts to IPython-specific settings.
109 """
109 """
110
110
111 @property
111 @property
112 def config(self):
112 def config(self):
113 return self.settings.get('config', None)
113 return self.settings.get('config', None)
114
114
115 @property
115 @property
116 def log(self):
116 def log(self):
117 """use the IPython log by default, falling back on tornado's logger"""
117 """use the IPython log by default, falling back on tornado's logger"""
118 if Application.initialized():
118 if Application.initialized():
119 return Application.instance().log
119 return Application.instance().log
120 else:
120 else:
121 return app_log
121 return app_log
122
123 @property
124 def jinja_template_vars(self):
125 """User-supplied values to supply to jinja templates."""
126 return self.settings.get('jinja_template_vars', {})
122
127
123 #---------------------------------------------------------------
128 #---------------------------------------------------------------
124 # URLs
129 # URLs
125 #---------------------------------------------------------------
130 #---------------------------------------------------------------
126
131
127 @property
132 @property
128 def version_hash(self):
133 def version_hash(self):
129 """The version hash to use for cache hints for static files"""
134 """The version hash to use for cache hints for static files"""
130 return self.settings.get('version_hash', '')
135 return self.settings.get('version_hash', '')
131
136
132 @property
137 @property
133 def mathjax_url(self):
138 def mathjax_url(self):
134 return self.settings.get('mathjax_url', '')
139 return self.settings.get('mathjax_url', '')
135
140
136 @property
141 @property
137 def base_url(self):
142 def base_url(self):
138 return self.settings.get('base_url', '/')
143 return self.settings.get('base_url', '/')
139
144
140 @property
145 @property
141 def default_url(self):
146 def default_url(self):
142 return self.settings.get('default_url', '')
147 return self.settings.get('default_url', '')
143
148
144 @property
149 @property
145 def ws_url(self):
150 def ws_url(self):
146 return self.settings.get('websocket_url', '')
151 return self.settings.get('websocket_url', '')
147
152
148 @property
153 @property
149 def contents_js_source(self):
154 def contents_js_source(self):
150 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
155 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
151 'services/contents'))
156 'services/contents'))
152 return self.settings.get('contents_js_source', 'services/contents')
157 return self.settings.get('contents_js_source', 'services/contents')
153
158
154 #---------------------------------------------------------------
159 #---------------------------------------------------------------
155 # Manager objects
160 # Manager objects
156 #---------------------------------------------------------------
161 #---------------------------------------------------------------
157
162
158 @property
163 @property
159 def kernel_manager(self):
164 def kernel_manager(self):
160 return self.settings['kernel_manager']
165 return self.settings['kernel_manager']
161
166
162 @property
167 @property
163 def contents_manager(self):
168 def contents_manager(self):
164 return self.settings['contents_manager']
169 return self.settings['contents_manager']
165
170
166 @property
171 @property
167 def cluster_manager(self):
172 def cluster_manager(self):
168 return self.settings['cluster_manager']
173 return self.settings['cluster_manager']
169
174
170 @property
175 @property
171 def session_manager(self):
176 def session_manager(self):
172 return self.settings['session_manager']
177 return self.settings['session_manager']
173
178
174 @property
179 @property
175 def terminal_manager(self):
180 def terminal_manager(self):
176 return self.settings['terminal_manager']
181 return self.settings['terminal_manager']
177
182
178 @property
183 @property
179 def kernel_spec_manager(self):
184 def kernel_spec_manager(self):
180 return self.settings['kernel_spec_manager']
185 return self.settings['kernel_spec_manager']
181
186
182 @property
187 @property
183 def config_manager(self):
188 def config_manager(self):
184 return self.settings['config_manager']
189 return self.settings['config_manager']
185
190
186 #---------------------------------------------------------------
191 #---------------------------------------------------------------
187 # CORS
192 # CORS
188 #---------------------------------------------------------------
193 #---------------------------------------------------------------
189
194
190 @property
195 @property
191 def allow_origin(self):
196 def allow_origin(self):
192 """Normal Access-Control-Allow-Origin"""
197 """Normal Access-Control-Allow-Origin"""
193 return self.settings.get('allow_origin', '')
198 return self.settings.get('allow_origin', '')
194
199
195 @property
200 @property
196 def allow_origin_pat(self):
201 def allow_origin_pat(self):
197 """Regular expression version of allow_origin"""
202 """Regular expression version of allow_origin"""
198 return self.settings.get('allow_origin_pat', None)
203 return self.settings.get('allow_origin_pat', None)
199
204
200 @property
205 @property
201 def allow_credentials(self):
206 def allow_credentials(self):
202 """Whether to set Access-Control-Allow-Credentials"""
207 """Whether to set Access-Control-Allow-Credentials"""
203 return self.settings.get('allow_credentials', False)
208 return self.settings.get('allow_credentials', False)
204
209
205 def set_default_headers(self):
210 def set_default_headers(self):
206 """Add CORS headers, if defined"""
211 """Add CORS headers, if defined"""
207 super(IPythonHandler, self).set_default_headers()
212 super(IPythonHandler, self).set_default_headers()
208 if self.allow_origin:
213 if self.allow_origin:
209 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
214 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
210 elif self.allow_origin_pat:
215 elif self.allow_origin_pat:
211 origin = self.get_origin()
216 origin = self.get_origin()
212 if origin and self.allow_origin_pat.match(origin):
217 if origin and self.allow_origin_pat.match(origin):
213 self.set_header("Access-Control-Allow-Origin", origin)
218 self.set_header("Access-Control-Allow-Origin", origin)
214 if self.allow_credentials:
219 if self.allow_credentials:
215 self.set_header("Access-Control-Allow-Credentials", 'true')
220 self.set_header("Access-Control-Allow-Credentials", 'true')
216
221
217 def get_origin(self):
222 def get_origin(self):
218 # Handle WebSocket Origin naming convention differences
223 # Handle WebSocket Origin naming convention differences
219 # The difference between version 8 and 13 is that in 8 the
224 # The difference between version 8 and 13 is that in 8 the
220 # client sends a "Sec-Websocket-Origin" header and in 13 it's
225 # client sends a "Sec-Websocket-Origin" header and in 13 it's
221 # simply "Origin".
226 # simply "Origin".
222 if "Origin" in self.request.headers:
227 if "Origin" in self.request.headers:
223 origin = self.request.headers.get("Origin")
228 origin = self.request.headers.get("Origin")
224 else:
229 else:
225 origin = self.request.headers.get("Sec-Websocket-Origin", None)
230 origin = self.request.headers.get("Sec-Websocket-Origin", None)
226 return origin
231 return origin
227
232
228 #---------------------------------------------------------------
233 #---------------------------------------------------------------
229 # template rendering
234 # template rendering
230 #---------------------------------------------------------------
235 #---------------------------------------------------------------
231
236
232 def get_template(self, name):
237 def get_template(self, name):
233 """Return the jinja template object for a given name"""
238 """Return the jinja template object for a given name"""
234 return self.settings['jinja2_env'].get_template(name)
239 return self.settings['jinja2_env'].get_template(name)
235
240
236 def render_template(self, name, **ns):
241 def render_template(self, name, **ns):
237 ns.update(self.template_namespace)
242 ns.update(self.template_namespace)
238 template = self.get_template(name)
243 template = self.get_template(name)
239 return template.render(**ns)
244 return template.render(**ns)
240
245
241 @property
246 @property
242 def template_namespace(self):
247 def template_namespace(self):
243 return dict(
248 return dict(
244 base_url=self.base_url,
249 base_url=self.base_url,
245 default_url=self.default_url,
250 default_url=self.default_url,
246 ws_url=self.ws_url,
251 ws_url=self.ws_url,
247 logged_in=self.logged_in,
252 logged_in=self.logged_in,
248 login_available=self.login_available,
253 login_available=self.login_available,
249 static_url=self.static_url,
254 static_url=self.static_url,
250 sys_info=sys_info,
255 sys_info=sys_info,
251 contents_js_source=self.contents_js_source,
256 contents_js_source=self.contents_js_source,
252 version_hash=self.version_hash,
257 version_hash=self.version_hash,
258 **self.jinja_template_vars
253 )
259 )
254
260
255 def get_json_body(self):
261 def get_json_body(self):
256 """Return the body of the request as JSON data."""
262 """Return the body of the request as JSON data."""
257 if not self.request.body:
263 if not self.request.body:
258 return None
264 return None
259 # Do we need to call body.decode('utf-8') here?
265 # Do we need to call body.decode('utf-8') here?
260 body = self.request.body.strip().decode(u'utf-8')
266 body = self.request.body.strip().decode(u'utf-8')
261 try:
267 try:
262 model = json.loads(body)
268 model = json.loads(body)
263 except Exception:
269 except Exception:
264 self.log.debug("Bad JSON: %r", body)
270 self.log.debug("Bad JSON: %r", body)
265 self.log.error("Couldn't parse JSON", exc_info=True)
271 self.log.error("Couldn't parse JSON", exc_info=True)
266 raise web.HTTPError(400, u'Invalid JSON in body of request')
272 raise web.HTTPError(400, u'Invalid JSON in body of request')
267 return model
273 return model
268
274
269 def write_error(self, status_code, **kwargs):
275 def write_error(self, status_code, **kwargs):
270 """render custom error pages"""
276 """render custom error pages"""
271 exc_info = kwargs.get('exc_info')
277 exc_info = kwargs.get('exc_info')
272 message = ''
278 message = ''
273 status_message = responses.get(status_code, 'Unknown HTTP Error')
279 status_message = responses.get(status_code, 'Unknown HTTP Error')
274 if exc_info:
280 if exc_info:
275 exception = exc_info[1]
281 exception = exc_info[1]
276 # get the custom message, if defined
282 # get the custom message, if defined
277 try:
283 try:
278 message = exception.log_message % exception.args
284 message = exception.log_message % exception.args
279 except Exception:
285 except Exception:
280 pass
286 pass
281
287
282 # construct the custom reason, if defined
288 # construct the custom reason, if defined
283 reason = getattr(exception, 'reason', '')
289 reason = getattr(exception, 'reason', '')
284 if reason:
290 if reason:
285 status_message = reason
291 status_message = reason
286
292
287 # build template namespace
293 # build template namespace
288 ns = dict(
294 ns = dict(
289 status_code=status_code,
295 status_code=status_code,
290 status_message=status_message,
296 status_message=status_message,
291 message=message,
297 message=message,
292 exception=exception,
298 exception=exception,
293 )
299 )
294
300
295 self.set_header('Content-Type', 'text/html')
301 self.set_header('Content-Type', 'text/html')
296 # render the template
302 # render the template
297 try:
303 try:
298 html = self.render_template('%s.html' % status_code, **ns)
304 html = self.render_template('%s.html' % status_code, **ns)
299 except TemplateNotFound:
305 except TemplateNotFound:
300 self.log.debug("No template for %d", status_code)
306 self.log.debug("No template for %d", status_code)
301 html = self.render_template('error.html', **ns)
307 html = self.render_template('error.html', **ns)
302
308
303 self.write(html)
309 self.write(html)
304
310
305
311
306
312
307 class Template404(IPythonHandler):
313 class Template404(IPythonHandler):
308 """Render our 404 template"""
314 """Render our 404 template"""
309 def prepare(self):
315 def prepare(self):
310 raise web.HTTPError(404)
316 raise web.HTTPError(404)
311
317
312
318
313 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
319 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
314 """static files should only be accessible when logged in"""
320 """static files should only be accessible when logged in"""
315
321
316 @web.authenticated
322 @web.authenticated
317 def get(self, path):
323 def get(self, path):
318 if os.path.splitext(path)[1] == '.ipynb':
324 if os.path.splitext(path)[1] == '.ipynb':
319 name = path.rsplit('/', 1)[-1]
325 name = path.rsplit('/', 1)[-1]
320 self.set_header('Content-Type', 'application/json')
326 self.set_header('Content-Type', 'application/json')
321 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
327 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
322
328
323 return web.StaticFileHandler.get(self, path)
329 return web.StaticFileHandler.get(self, path)
324
330
325 def set_headers(self):
331 def set_headers(self):
326 super(AuthenticatedFileHandler, self).set_headers()
332 super(AuthenticatedFileHandler, self).set_headers()
327 # disable browser caching, rely on 304 replies for savings
333 # disable browser caching, rely on 304 replies for savings
328 if "v" not in self.request.arguments:
334 if "v" not in self.request.arguments:
329 self.add_header("Cache-Control", "no-cache")
335 self.add_header("Cache-Control", "no-cache")
330
336
331 def compute_etag(self):
337 def compute_etag(self):
332 return None
338 return None
333
339
334 def validate_absolute_path(self, root, absolute_path):
340 def validate_absolute_path(self, root, absolute_path):
335 """Validate and return the absolute path.
341 """Validate and return the absolute path.
336
342
337 Requires tornado 3.1
343 Requires tornado 3.1
338
344
339 Adding to tornado's own handling, forbids the serving of hidden files.
345 Adding to tornado's own handling, forbids the serving of hidden files.
340 """
346 """
341 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
347 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
342 abs_root = os.path.abspath(root)
348 abs_root = os.path.abspath(root)
343 if is_hidden(abs_path, abs_root):
349 if is_hidden(abs_path, abs_root):
344 self.log.info("Refusing to serve hidden file, via 404 Error")
350 self.log.info("Refusing to serve hidden file, via 404 Error")
345 raise web.HTTPError(404)
351 raise web.HTTPError(404)
346 return abs_path
352 return abs_path
347
353
348
354
349 def json_errors(method):
355 def json_errors(method):
350 """Decorate methods with this to return GitHub style JSON errors.
356 """Decorate methods with this to return GitHub style JSON errors.
351
357
352 This should be used on any JSON API on any handler method that can raise HTTPErrors.
358 This should be used on any JSON API on any handler method that can raise HTTPErrors.
353
359
354 This will grab the latest HTTPError exception using sys.exc_info
360 This will grab the latest HTTPError exception using sys.exc_info
355 and then:
361 and then:
356
362
357 1. Set the HTTP status code based on the HTTPError
363 1. Set the HTTP status code based on the HTTPError
358 2. Create and return a JSON body with a message field describing
364 2. Create and return a JSON body with a message field describing
359 the error in a human readable form.
365 the error in a human readable form.
360 """
366 """
361 @functools.wraps(method)
367 @functools.wraps(method)
362 @gen.coroutine
368 @gen.coroutine
363 def wrapper(self, *args, **kwargs):
369 def wrapper(self, *args, **kwargs):
364 try:
370 try:
365 result = yield gen.maybe_future(method(self, *args, **kwargs))
371 result = yield gen.maybe_future(method(self, *args, **kwargs))
366 except web.HTTPError as e:
372 except web.HTTPError as e:
367 status = e.status_code
373 status = e.status_code
368 message = e.log_message
374 message = e.log_message
369 self.log.warn(message)
375 self.log.warn(message)
370 self.set_status(e.status_code)
376 self.set_status(e.status_code)
371 reply = dict(message=message, reason=e.reason)
377 reply = dict(message=message, reason=e.reason)
372 self.finish(json.dumps(reply))
378 self.finish(json.dumps(reply))
373 except Exception:
379 except Exception:
374 self.log.error("Unhandled error in API request", exc_info=True)
380 self.log.error("Unhandled error in API request", exc_info=True)
375 status = 500
381 status = 500
376 message = "Unknown server error"
382 message = "Unknown server error"
377 t, value, tb = sys.exc_info()
383 t, value, tb = sys.exc_info()
378 self.set_status(status)
384 self.set_status(status)
379 tb_text = ''.join(traceback.format_exception(t, value, tb))
385 tb_text = ''.join(traceback.format_exception(t, value, tb))
380 reply = dict(message=message, reason=None, traceback=tb_text)
386 reply = dict(message=message, reason=None, traceback=tb_text)
381 self.finish(json.dumps(reply))
387 self.finish(json.dumps(reply))
382 else:
388 else:
383 # FIXME: can use regular return in generators in py3
389 # FIXME: can use regular return in generators in py3
384 raise gen.Return(result)
390 raise gen.Return(result)
385 return wrapper
391 return wrapper
386
392
387
393
388
394
389 #-----------------------------------------------------------------------------
395 #-----------------------------------------------------------------------------
390 # File handler
396 # File handler
391 #-----------------------------------------------------------------------------
397 #-----------------------------------------------------------------------------
392
398
393 # to minimize subclass changes:
399 # to minimize subclass changes:
394 HTTPError = web.HTTPError
400 HTTPError = web.HTTPError
395
401
396 class FileFindHandler(web.StaticFileHandler):
402 class FileFindHandler(web.StaticFileHandler):
397 """subclass of StaticFileHandler for serving files from a search path"""
403 """subclass of StaticFileHandler for serving files from a search path"""
398
404
399 # cache search results, don't search for files more than once
405 # cache search results, don't search for files more than once
400 _static_paths = {}
406 _static_paths = {}
401
407
402 def set_headers(self):
408 def set_headers(self):
403 super(FileFindHandler, self).set_headers()
409 super(FileFindHandler, self).set_headers()
404 # disable browser caching, rely on 304 replies for savings
410 # disable browser caching, rely on 304 replies for savings
405 if "v" not in self.request.arguments or \
411 if "v" not in self.request.arguments or \
406 any(self.request.path.startswith(path) for path in self.no_cache_paths):
412 any(self.request.path.startswith(path) for path in self.no_cache_paths):
407 self.set_header("Cache-Control", "no-cache")
413 self.set_header("Cache-Control", "no-cache")
408
414
409 def initialize(self, path, default_filename=None, no_cache_paths=None):
415 def initialize(self, path, default_filename=None, no_cache_paths=None):
410 self.no_cache_paths = no_cache_paths or []
416 self.no_cache_paths = no_cache_paths or []
411
417
412 if isinstance(path, string_types):
418 if isinstance(path, string_types):
413 path = [path]
419 path = [path]
414
420
415 self.root = tuple(
421 self.root = tuple(
416 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
422 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
417 )
423 )
418 self.default_filename = default_filename
424 self.default_filename = default_filename
419
425
420 def compute_etag(self):
426 def compute_etag(self):
421 return None
427 return None
422
428
423 @classmethod
429 @classmethod
424 def get_absolute_path(cls, roots, path):
430 def get_absolute_path(cls, roots, path):
425 """locate a file to serve on our static file search path"""
431 """locate a file to serve on our static file search path"""
426 with cls._lock:
432 with cls._lock:
427 if path in cls._static_paths:
433 if path in cls._static_paths:
428 return cls._static_paths[path]
434 return cls._static_paths[path]
429 try:
435 try:
430 abspath = os.path.abspath(filefind(path, roots))
436 abspath = os.path.abspath(filefind(path, roots))
431 except IOError:
437 except IOError:
432 # IOError means not found
438 # IOError means not found
433 return ''
439 return ''
434
440
435 cls._static_paths[path] = abspath
441 cls._static_paths[path] = abspath
436 return abspath
442 return abspath
437
443
438 def validate_absolute_path(self, root, absolute_path):
444 def validate_absolute_path(self, root, absolute_path):
439 """check if the file should be served (raises 404, 403, etc.)"""
445 """check if the file should be served (raises 404, 403, etc.)"""
440 if absolute_path == '':
446 if absolute_path == '':
441 raise web.HTTPError(404)
447 raise web.HTTPError(404)
442
448
443 for root in self.root:
449 for root in self.root:
444 if (absolute_path + os.sep).startswith(root):
450 if (absolute_path + os.sep).startswith(root):
445 break
451 break
446
452
447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
453 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
448
454
449
455
450 class ApiVersionHandler(IPythonHandler):
456 class ApiVersionHandler(IPythonHandler):
451
457
452 @json_errors
458 @json_errors
453 def get(self):
459 def get(self):
454 # not authenticated, so give as few info as possible
460 # not authenticated, so give as few info as possible
455 self.finish(json.dumps({"version":IPython.__version__}))
461 self.finish(json.dumps({"version":IPython.__version__}))
456
462
457
463
458 class TrailingSlashHandler(web.RequestHandler):
464 class TrailingSlashHandler(web.RequestHandler):
459 """Simple redirect handler that strips trailing slashes
465 """Simple redirect handler that strips trailing slashes
460
466
461 This should be the first, highest priority handler.
467 This should be the first, highest priority handler.
462 """
468 """
463
469
464 def get(self):
470 def get(self):
465 self.redirect(self.request.uri.rstrip('/'))
471 self.redirect(self.request.uri.rstrip('/'))
466
472
467 post = put = get
473 post = put = get
468
474
469
475
470 class FilesRedirectHandler(IPythonHandler):
476 class FilesRedirectHandler(IPythonHandler):
471 """Handler for redirecting relative URLs to the /files/ handler"""
477 """Handler for redirecting relative URLs to the /files/ handler"""
472
478
473 @staticmethod
479 @staticmethod
474 def redirect_to_files(self, path):
480 def redirect_to_files(self, path):
475 """make redirect logic a reusable static method
481 """make redirect logic a reusable static method
476
482
477 so it can be called from other handlers.
483 so it can be called from other handlers.
478 """
484 """
479 cm = self.contents_manager
485 cm = self.contents_manager
480 if cm.dir_exists(path):
486 if cm.dir_exists(path):
481 # it's a *directory*, redirect to /tree
487 # it's a *directory*, redirect to /tree
482 url = url_path_join(self.base_url, 'tree', path)
488 url = url_path_join(self.base_url, 'tree', path)
483 else:
489 else:
484 orig_path = path
490 orig_path = path
485 # otherwise, redirect to /files
491 # otherwise, redirect to /files
486 parts = path.split('/')
492 parts = path.split('/')
487
493
488 if not cm.file_exists(path=path) and 'files' in parts:
494 if not cm.file_exists(path=path) and 'files' in parts:
489 # redirect without files/ iff it would 404
495 # redirect without files/ iff it would 404
490 # this preserves pre-2.0-style 'files/' links
496 # this preserves pre-2.0-style 'files/' links
491 self.log.warn("Deprecated files/ URL: %s", orig_path)
497 self.log.warn("Deprecated files/ URL: %s", orig_path)
492 parts.remove('files')
498 parts.remove('files')
493 path = '/'.join(parts)
499 path = '/'.join(parts)
494
500
495 if not cm.file_exists(path=path):
501 if not cm.file_exists(path=path):
496 raise web.HTTPError(404)
502 raise web.HTTPError(404)
497
503
498 url = url_path_join(self.base_url, 'files', path)
504 url = url_path_join(self.base_url, 'files', path)
499 url = url_escape(url)
505 url = url_escape(url)
500 self.log.debug("Redirecting %s to %s", self.request.path, url)
506 self.log.debug("Redirecting %s to %s", self.request.path, url)
501 self.redirect(url)
507 self.redirect(url)
502
508
503 def get(self, path=''):
509 def get(self, path=''):
504 return self.redirect_to_files(self, path)
510 return self.redirect_to_files(self, path)
505
511
506
512
507 #-----------------------------------------------------------------------------
513 #-----------------------------------------------------------------------------
508 # URL pattern fragments for re-use
514 # URL pattern fragments for re-use
509 #-----------------------------------------------------------------------------
515 #-----------------------------------------------------------------------------
510
516
511 # path matches any number of `/foo[/bar...]` or just `/` or ''
517 # path matches any number of `/foo[/bar...]` or just `/` or ''
512 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
518 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
513
519
514 #-----------------------------------------------------------------------------
520 #-----------------------------------------------------------------------------
515 # URL to handler mappings
521 # URL to handler mappings
516 #-----------------------------------------------------------------------------
522 #-----------------------------------------------------------------------------
517
523
518
524
519 default_handlers = [
525 default_handlers = [
520 (r".*/", TrailingSlashHandler),
526 (r".*/", TrailingSlashHandler),
521 (r"api", ApiVersionHandler)
527 (r"api", ApiVersionHandler)
522 ]
528 ]
@@ -1,1131 +1,1137 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import datetime
10 import datetime
11 import errno
11 import errno
12 import importlib
12 import importlib
13 import io
13 import io
14 import json
14 import json
15 import logging
15 import logging
16 import os
16 import os
17 import random
17 import random
18 import re
18 import re
19 import select
19 import select
20 import signal
20 import signal
21 import socket
21 import socket
22 import ssl
22 import ssl
23 import sys
23 import sys
24 import threading
24 import threading
25 import webbrowser
25 import webbrowser
26
26
27
27
28 # check for pyzmq
28 # check for pyzmq
29 from IPython.utils.zmqrelated import check_for_zmq
29 from IPython.utils.zmqrelated import check_for_zmq
30 check_for_zmq('13', 'IPython.html')
30 check_for_zmq('13', 'IPython.html')
31
31
32 from jinja2 import Environment, FileSystemLoader
32 from jinja2 import Environment, FileSystemLoader
33
33
34 # Install the pyzmq ioloop. This has to be done before anything else from
34 # Install the pyzmq ioloop. This has to be done before anything else from
35 # tornado is imported.
35 # tornado is imported.
36 from zmq.eventloop import ioloop
36 from zmq.eventloop import ioloop
37 ioloop.install()
37 ioloop.install()
38
38
39 # check for tornado 3.1.0
39 # check for tornado 3.1.0
40 msg = "The IPython Notebook requires tornado >= 4.0"
40 msg = "The IPython Notebook requires tornado >= 4.0"
41 try:
41 try:
42 import tornado
42 import tornado
43 except ImportError:
43 except ImportError:
44 raise ImportError(msg)
44 raise ImportError(msg)
45 try:
45 try:
46 version_info = tornado.version_info
46 version_info = tornado.version_info
47 except AttributeError:
47 except AttributeError:
48 raise ImportError(msg + ", but you have < 1.1.0")
48 raise ImportError(msg + ", but you have < 1.1.0")
49 if version_info < (4,0):
49 if version_info < (4,0):
50 raise ImportError(msg + ", but you have %s" % tornado.version)
50 raise ImportError(msg + ", but you have %s" % tornado.version)
51
51
52 from tornado import httpserver
52 from tornado import httpserver
53 from tornado import web
53 from tornado import web
54 from tornado.log import LogFormatter, app_log, access_log, gen_log
54 from tornado.log import LogFormatter, app_log, access_log, gen_log
55
55
56 from IPython.html import (
56 from IPython.html import (
57 DEFAULT_STATIC_FILES_PATH,
57 DEFAULT_STATIC_FILES_PATH,
58 DEFAULT_TEMPLATE_PATH_LIST,
58 DEFAULT_TEMPLATE_PATH_LIST,
59 )
59 )
60 from .base.handlers import Template404
60 from .base.handlers import Template404
61 from .log import log_request
61 from .log import log_request
62 from .services.kernels.kernelmanager import MappingKernelManager
62 from .services.kernels.kernelmanager import MappingKernelManager
63 from .services.config import ConfigManager
63 from .services.config import ConfigManager
64 from .services.contents.manager import ContentsManager
64 from .services.contents.manager import ContentsManager
65 from .services.contents.filemanager import FileContentsManager
65 from .services.contents.filemanager import FileContentsManager
66 from .services.clusters.clustermanager import ClusterManager
66 from .services.clusters.clustermanager import ClusterManager
67 from .services.sessions.sessionmanager import SessionManager
67 from .services.sessions.sessionmanager import SessionManager
68
68
69 from .auth.login import LoginHandler
69 from .auth.login import LoginHandler
70 from .auth.logout import LogoutHandler
70 from .auth.logout import LogoutHandler
71 from .base.handlers import IPythonHandler, FileFindHandler
71 from .base.handlers import IPythonHandler, FileFindHandler
72
72
73 from IPython.config import Config
73 from IPython.config import Config
74 from IPython.config.application import catch_config_error, boolean_flag
74 from IPython.config.application import catch_config_error, boolean_flag
75 from IPython.core.application import (
75 from IPython.core.application import (
76 BaseIPythonApplication, base_flags, base_aliases,
76 BaseIPythonApplication, base_flags, base_aliases,
77 )
77 )
78 from IPython.core.profiledir import ProfileDir
78 from IPython.core.profiledir import ProfileDir
79 from IPython.kernel import KernelManager
79 from IPython.kernel import KernelManager
80 from IPython.kernel.kernelspec import KernelSpecManager
80 from IPython.kernel.kernelspec import KernelSpecManager
81 from IPython.kernel.zmq.session import Session
81 from IPython.kernel.zmq.session import Session
82 from IPython.nbformat.sign import NotebookNotary
82 from IPython.nbformat.sign import NotebookNotary
83 from IPython.utils.importstring import import_item
83 from IPython.utils.importstring import import_item
84 from IPython.utils import submodule
84 from IPython.utils import submodule
85 from IPython.utils.process import check_pid
85 from IPython.utils.process import check_pid
86 from IPython.utils.traitlets import (
86 from IPython.utils.traitlets import (
87 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
87 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
88 TraitError, Type,
88 TraitError, Type,
89 )
89 )
90 from IPython.utils import py3compat
90 from IPython.utils import py3compat
91 from IPython.utils.path import filefind, get_ipython_dir
91 from IPython.utils.path import filefind, get_ipython_dir
92 from IPython.utils.sysinfo import get_sys_info
92 from IPython.utils.sysinfo import get_sys_info
93
93
94 from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS
94 from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS
95 from .utils import url_path_join
95 from .utils import url_path_join
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Module globals
98 # Module globals
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 _examples = """
101 _examples = """
102 ipython notebook # start the notebook
102 ipython notebook # start the notebook
103 ipython notebook --profile=sympy # use the sympy profile
103 ipython notebook --profile=sympy # use the sympy profile
104 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
104 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
105 """
105 """
106
106
107 #-----------------------------------------------------------------------------
107 #-----------------------------------------------------------------------------
108 # Helper functions
108 # Helper functions
109 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
110
110
111 def random_ports(port, n):
111 def random_ports(port, n):
112 """Generate a list of n random ports near the given port.
112 """Generate a list of n random ports near the given port.
113
113
114 The first 5 ports will be sequential, and the remaining n-5 will be
114 The first 5 ports will be sequential, and the remaining n-5 will be
115 randomly selected in the range [port-2*n, port+2*n].
115 randomly selected in the range [port-2*n, port+2*n].
116 """
116 """
117 for i in range(min(5, n)):
117 for i in range(min(5, n)):
118 yield port + i
118 yield port + i
119 for i in range(n-5):
119 for i in range(n-5):
120 yield max(1, port + random.randint(-2*n, 2*n))
120 yield max(1, port + random.randint(-2*n, 2*n))
121
121
122 def load_handlers(name):
122 def load_handlers(name):
123 """Load the (URL pattern, handler) tuples for each component."""
123 """Load the (URL pattern, handler) tuples for each component."""
124 name = 'IPython.html.' + name
124 name = 'IPython.html.' + name
125 mod = __import__(name, fromlist=['default_handlers'])
125 mod = __import__(name, fromlist=['default_handlers'])
126 return mod.default_handlers
126 return mod.default_handlers
127
127
128 #-----------------------------------------------------------------------------
128 #-----------------------------------------------------------------------------
129 # The Tornado web application
129 # The Tornado web application
130 #-----------------------------------------------------------------------------
130 #-----------------------------------------------------------------------------
131
131
132 class NotebookWebApplication(web.Application):
132 class NotebookWebApplication(web.Application):
133
133
134 def __init__(self, ipython_app, kernel_manager, contents_manager,
134 def __init__(self, ipython_app, kernel_manager, contents_manager,
135 cluster_manager, session_manager, kernel_spec_manager,
135 cluster_manager, session_manager, kernel_spec_manager,
136 config_manager, log,
136 config_manager, log,
137 base_url, default_url, settings_overrides, jinja_env_options):
137 base_url, default_url, settings_overrides, jinja_env_options):
138
138
139 settings = self.init_settings(
139 settings = self.init_settings(
140 ipython_app, kernel_manager, contents_manager, cluster_manager,
140 ipython_app, kernel_manager, contents_manager, cluster_manager,
141 session_manager, kernel_spec_manager, config_manager, log, base_url,
141 session_manager, kernel_spec_manager, config_manager, log, base_url,
142 default_url, settings_overrides, jinja_env_options)
142 default_url, settings_overrides, jinja_env_options)
143 handlers = self.init_handlers(settings)
143 handlers = self.init_handlers(settings)
144
144
145 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 super(NotebookWebApplication, self).__init__(handlers, **settings)
146
146
147 def init_settings(self, ipython_app, kernel_manager, contents_manager,
147 def init_settings(self, ipython_app, kernel_manager, contents_manager,
148 cluster_manager, session_manager, kernel_spec_manager,
148 cluster_manager, session_manager, kernel_spec_manager,
149 config_manager,
149 config_manager,
150 log, base_url, default_url, settings_overrides,
150 log, base_url, default_url, settings_overrides,
151 jinja_env_options=None):
151 jinja_env_options=None):
152
152
153 _template_path = settings_overrides.get(
153 _template_path = settings_overrides.get(
154 "template_path",
154 "template_path",
155 ipython_app.template_file_path,
155 ipython_app.template_file_path,
156 )
156 )
157 if isinstance(_template_path, str):
157 if isinstance(_template_path, str):
158 _template_path = (_template_path,)
158 _template_path = (_template_path,)
159 template_path = [os.path.expanduser(path) for path in _template_path]
159 template_path = [os.path.expanduser(path) for path in _template_path]
160
160
161 jenv_opt = jinja_env_options if jinja_env_options else {}
161 jenv_opt = jinja_env_options if jinja_env_options else {}
162 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
162 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
163
163
164 sys_info = get_sys_info()
164 sys_info = get_sys_info()
165 if sys_info['commit_source'] == 'repository':
165 if sys_info['commit_source'] == 'repository':
166 # don't cache (rely on 304) when working from master
166 # don't cache (rely on 304) when working from master
167 version_hash = ''
167 version_hash = ''
168 else:
168 else:
169 # reset the cache on server restart
169 # reset the cache on server restart
170 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
170 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
171
171
172 settings = dict(
172 settings = dict(
173 # basics
173 # basics
174 log_function=log_request,
174 log_function=log_request,
175 base_url=base_url,
175 base_url=base_url,
176 default_url=default_url,
176 default_url=default_url,
177 template_path=template_path,
177 template_path=template_path,
178 static_path=ipython_app.static_file_path,
178 static_path=ipython_app.static_file_path,
179 static_handler_class = FileFindHandler,
179 static_handler_class = FileFindHandler,
180 static_url_prefix = url_path_join(base_url,'/static/'),
180 static_url_prefix = url_path_join(base_url,'/static/'),
181 static_handler_args = {
181 static_handler_args = {
182 # don't cache custom.js
182 # don't cache custom.js
183 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
183 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
184 },
184 },
185 version_hash=version_hash,
185 version_hash=version_hash,
186
186
187 # authentication
187 # authentication
188 cookie_secret=ipython_app.cookie_secret,
188 cookie_secret=ipython_app.cookie_secret,
189 login_url=url_path_join(base_url,'/login'),
189 login_url=url_path_join(base_url,'/login'),
190 login_handler_class=ipython_app.login_handler_class,
190 login_handler_class=ipython_app.login_handler_class,
191 logout_handler_class=ipython_app.logout_handler_class,
191 logout_handler_class=ipython_app.logout_handler_class,
192 password=ipython_app.password,
192 password=ipython_app.password,
193
193
194 # managers
194 # managers
195 kernel_manager=kernel_manager,
195 kernel_manager=kernel_manager,
196 contents_manager=contents_manager,
196 contents_manager=contents_manager,
197 cluster_manager=cluster_manager,
197 cluster_manager=cluster_manager,
198 session_manager=session_manager,
198 session_manager=session_manager,
199 kernel_spec_manager=kernel_spec_manager,
199 kernel_spec_manager=kernel_spec_manager,
200 config_manager=config_manager,
200 config_manager=config_manager,
201
201
202 # IPython stuff
202 # IPython stuff
203 jinja_template_vars=ipython_app.jinja_template_vars,
203 nbextensions_path=ipython_app.nbextensions_path,
204 nbextensions_path=ipython_app.nbextensions_path,
204 websocket_url=ipython_app.websocket_url,
205 websocket_url=ipython_app.websocket_url,
205 mathjax_url=ipython_app.mathjax_url,
206 mathjax_url=ipython_app.mathjax_url,
206 config=ipython_app.config,
207 config=ipython_app.config,
207 jinja2_env=env,
208 jinja2_env=env,
208 terminals_available=False, # Set later if terminals are available
209 terminals_available=False, # Set later if terminals are available
209 )
210 )
210
211
211 # allow custom overrides for the tornado web app.
212 # allow custom overrides for the tornado web app.
212 settings.update(settings_overrides)
213 settings.update(settings_overrides)
213 return settings
214 return settings
214
215
215 def init_handlers(self, settings):
216 def init_handlers(self, settings):
216 """Load the (URL pattern, handler) tuples for each component."""
217 """Load the (URL pattern, handler) tuples for each component."""
217
218
218 # Order matters. The first handler to match the URL will handle the request.
219 # Order matters. The first handler to match the URL will handle the request.
219 handlers = []
220 handlers = []
220 handlers.extend(load_handlers('tree.handlers'))
221 handlers.extend(load_handlers('tree.handlers'))
221 handlers.extend([(r"/login", settings['login_handler_class'])])
222 handlers.extend([(r"/login", settings['login_handler_class'])])
222 handlers.extend([(r"/logout", settings['logout_handler_class'])])
223 handlers.extend([(r"/logout", settings['logout_handler_class'])])
223 handlers.extend(load_handlers('files.handlers'))
224 handlers.extend(load_handlers('files.handlers'))
224 handlers.extend(load_handlers('notebook.handlers'))
225 handlers.extend(load_handlers('notebook.handlers'))
225 handlers.extend(load_handlers('nbconvert.handlers'))
226 handlers.extend(load_handlers('nbconvert.handlers'))
226 handlers.extend(load_handlers('kernelspecs.handlers'))
227 handlers.extend(load_handlers('kernelspecs.handlers'))
227 handlers.extend(load_handlers('edit.handlers'))
228 handlers.extend(load_handlers('edit.handlers'))
228 handlers.extend(load_handlers('services.config.handlers'))
229 handlers.extend(load_handlers('services.config.handlers'))
229 handlers.extend(load_handlers('services.kernels.handlers'))
230 handlers.extend(load_handlers('services.kernels.handlers'))
230 handlers.extend(load_handlers('services.contents.handlers'))
231 handlers.extend(load_handlers('services.contents.handlers'))
231 handlers.extend(load_handlers('services.clusters.handlers'))
232 handlers.extend(load_handlers('services.clusters.handlers'))
232 handlers.extend(load_handlers('services.sessions.handlers'))
233 handlers.extend(load_handlers('services.sessions.handlers'))
233 handlers.extend(load_handlers('services.nbconvert.handlers'))
234 handlers.extend(load_handlers('services.nbconvert.handlers'))
234 handlers.extend(load_handlers('services.kernelspecs.handlers'))
235 handlers.extend(load_handlers('services.kernelspecs.handlers'))
235 handlers.extend(load_handlers('services.security.handlers'))
236 handlers.extend(load_handlers('services.security.handlers'))
236 handlers.append(
237 handlers.append(
237 (r"/nbextensions/(.*)", FileFindHandler, {
238 (r"/nbextensions/(.*)", FileFindHandler, {
238 'path': settings['nbextensions_path'],
239 'path': settings['nbextensions_path'],
239 'no_cache_paths': ['/'], # don't cache anything in nbextensions
240 'no_cache_paths': ['/'], # don't cache anything in nbextensions
240 }),
241 }),
241 )
242 )
242 # register base handlers last
243 # register base handlers last
243 handlers.extend(load_handlers('base.handlers'))
244 handlers.extend(load_handlers('base.handlers'))
244 # set the URL that will be redirected from `/`
245 # set the URL that will be redirected from `/`
245 handlers.append(
246 handlers.append(
246 (r'/?', web.RedirectHandler, {
247 (r'/?', web.RedirectHandler, {
247 'url' : settings['default_url'],
248 'url' : settings['default_url'],
248 'permanent': False, # want 302, not 301
249 'permanent': False, # want 302, not 301
249 })
250 })
250 )
251 )
251 # prepend base_url onto the patterns that we match
252 # prepend base_url onto the patterns that we match
252 new_handlers = []
253 new_handlers = []
253 for handler in handlers:
254 for handler in handlers:
254 pattern = url_path_join(settings['base_url'], handler[0])
255 pattern = url_path_join(settings['base_url'], handler[0])
255 new_handler = tuple([pattern] + list(handler[1:]))
256 new_handler = tuple([pattern] + list(handler[1:]))
256 new_handlers.append(new_handler)
257 new_handlers.append(new_handler)
257 # add 404 on the end, which will catch everything that falls through
258 # add 404 on the end, which will catch everything that falls through
258 new_handlers.append((r'(.*)', Template404))
259 new_handlers.append((r'(.*)', Template404))
259 return new_handlers
260 return new_handlers
260
261
261
262
262 class NbserverListApp(BaseIPythonApplication):
263 class NbserverListApp(BaseIPythonApplication):
263
264
264 description="List currently running notebook servers in this profile."
265 description="List currently running notebook servers in this profile."
265
266
266 flags = dict(
267 flags = dict(
267 json=({'NbserverListApp': {'json': True}},
268 json=({'NbserverListApp': {'json': True}},
268 "Produce machine-readable JSON output."),
269 "Produce machine-readable JSON output."),
269 )
270 )
270
271
271 json = Bool(False, config=True,
272 json = Bool(False, config=True,
272 help="If True, each line of output will be a JSON object with the "
273 help="If True, each line of output will be a JSON object with the "
273 "details from the server info file.")
274 "details from the server info file.")
274
275
275 def start(self):
276 def start(self):
276 if not self.json:
277 if not self.json:
277 print("Currently running servers:")
278 print("Currently running servers:")
278 for serverinfo in list_running_servers(self.profile):
279 for serverinfo in list_running_servers(self.profile):
279 if self.json:
280 if self.json:
280 print(json.dumps(serverinfo))
281 print(json.dumps(serverinfo))
281 else:
282 else:
282 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
283 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
283
284
284 #-----------------------------------------------------------------------------
285 #-----------------------------------------------------------------------------
285 # Aliases and Flags
286 # Aliases and Flags
286 #-----------------------------------------------------------------------------
287 #-----------------------------------------------------------------------------
287
288
288 flags = dict(base_flags)
289 flags = dict(base_flags)
289 flags['no-browser']=(
290 flags['no-browser']=(
290 {'NotebookApp' : {'open_browser' : False}},
291 {'NotebookApp' : {'open_browser' : False}},
291 "Don't open the notebook in a browser after startup."
292 "Don't open the notebook in a browser after startup."
292 )
293 )
293 flags['pylab']=(
294 flags['pylab']=(
294 {'NotebookApp' : {'pylab' : 'warn'}},
295 {'NotebookApp' : {'pylab' : 'warn'}},
295 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
296 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
296 )
297 )
297 flags['no-mathjax']=(
298 flags['no-mathjax']=(
298 {'NotebookApp' : {'enable_mathjax' : False}},
299 {'NotebookApp' : {'enable_mathjax' : False}},
299 """Disable MathJax
300 """Disable MathJax
300
301
301 MathJax is the javascript library IPython uses to render math/LaTeX. It is
302 MathJax is the javascript library IPython uses to render math/LaTeX. It is
302 very large, so you may want to disable it if you have a slow internet
303 very large, so you may want to disable it if you have a slow internet
303 connection, or for offline use of the notebook.
304 connection, or for offline use of the notebook.
304
305
305 When disabled, equations etc. will appear as their untransformed TeX source.
306 When disabled, equations etc. will appear as their untransformed TeX source.
306 """
307 """
307 )
308 )
308
309
309 # Add notebook manager flags
310 # Add notebook manager flags
310 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
311 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
311 'DEPRECATED, IGNORED',
312 'DEPRECATED, IGNORED',
312 'DEPRECATED, IGNORED'))
313 'DEPRECATED, IGNORED'))
313
314
314 aliases = dict(base_aliases)
315 aliases = dict(base_aliases)
315
316
316 aliases.update({
317 aliases.update({
317 'ip': 'NotebookApp.ip',
318 'ip': 'NotebookApp.ip',
318 'port': 'NotebookApp.port',
319 'port': 'NotebookApp.port',
319 'port-retries': 'NotebookApp.port_retries',
320 'port-retries': 'NotebookApp.port_retries',
320 'transport': 'KernelManager.transport',
321 'transport': 'KernelManager.transport',
321 'keyfile': 'NotebookApp.keyfile',
322 'keyfile': 'NotebookApp.keyfile',
322 'certfile': 'NotebookApp.certfile',
323 'certfile': 'NotebookApp.certfile',
323 'notebook-dir': 'NotebookApp.notebook_dir',
324 'notebook-dir': 'NotebookApp.notebook_dir',
324 'browser': 'NotebookApp.browser',
325 'browser': 'NotebookApp.browser',
325 'pylab': 'NotebookApp.pylab',
326 'pylab': 'NotebookApp.pylab',
326 })
327 })
327
328
328 #-----------------------------------------------------------------------------
329 #-----------------------------------------------------------------------------
329 # NotebookApp
330 # NotebookApp
330 #-----------------------------------------------------------------------------
331 #-----------------------------------------------------------------------------
331
332
332 class NotebookApp(BaseIPythonApplication):
333 class NotebookApp(BaseIPythonApplication):
333
334
334 name = 'ipython-notebook'
335 name = 'ipython-notebook'
335
336
336 description = """
337 description = """
337 The IPython HTML Notebook.
338 The IPython HTML Notebook.
338
339
339 This launches a Tornado based HTML Notebook Server that serves up an
340 This launches a Tornado based HTML Notebook Server that serves up an
340 HTML5/Javascript Notebook client.
341 HTML5/Javascript Notebook client.
341 """
342 """
342 examples = _examples
343 examples = _examples
343 aliases = aliases
344 aliases = aliases
344 flags = flags
345 flags = flags
345
346
346 classes = [
347 classes = [
347 KernelManager, ProfileDir, Session, MappingKernelManager,
348 KernelManager, ProfileDir, Session, MappingKernelManager,
348 ContentsManager, FileContentsManager, NotebookNotary,
349 ContentsManager, FileContentsManager, NotebookNotary,
349 KernelSpecManager,
350 KernelSpecManager,
350 ]
351 ]
351 flags = Dict(flags)
352 flags = Dict(flags)
352 aliases = Dict(aliases)
353 aliases = Dict(aliases)
353
354
354 subcommands = dict(
355 subcommands = dict(
355 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
356 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
356 )
357 )
357
358
358 _log_formatter_cls = LogFormatter
359 _log_formatter_cls = LogFormatter
359
360
360 def _log_level_default(self):
361 def _log_level_default(self):
361 return logging.INFO
362 return logging.INFO
362
363
363 def _log_datefmt_default(self):
364 def _log_datefmt_default(self):
364 """Exclude date from default date format"""
365 """Exclude date from default date format"""
365 return "%H:%M:%S"
366 return "%H:%M:%S"
366
367
367 def _log_format_default(self):
368 def _log_format_default(self):
368 """override default log format to include time"""
369 """override default log format to include time"""
369 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
370 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
370
371
371 # create requested profiles by default, if they don't exist:
372 # create requested profiles by default, if they don't exist:
372 auto_create = Bool(True)
373 auto_create = Bool(True)
373
374
374 # file to be opened in the notebook server
375 # file to be opened in the notebook server
375 file_to_run = Unicode('', config=True)
376 file_to_run = Unicode('', config=True)
376
377
377 # Network related information
378 # Network related information
378
379
379 allow_origin = Unicode('', config=True,
380 allow_origin = Unicode('', config=True,
380 help="""Set the Access-Control-Allow-Origin header
381 help="""Set the Access-Control-Allow-Origin header
381
382
382 Use '*' to allow any origin to access your server.
383 Use '*' to allow any origin to access your server.
383
384
384 Takes precedence over allow_origin_pat.
385 Takes precedence over allow_origin_pat.
385 """
386 """
386 )
387 )
387
388
388 allow_origin_pat = Unicode('', config=True,
389 allow_origin_pat = Unicode('', config=True,
389 help="""Use a regular expression for the Access-Control-Allow-Origin header
390 help="""Use a regular expression for the Access-Control-Allow-Origin header
390
391
391 Requests from an origin matching the expression will get replies with:
392 Requests from an origin matching the expression will get replies with:
392
393
393 Access-Control-Allow-Origin: origin
394 Access-Control-Allow-Origin: origin
394
395
395 where `origin` is the origin of the request.
396 where `origin` is the origin of the request.
396
397
397 Ignored if allow_origin is set.
398 Ignored if allow_origin is set.
398 """
399 """
399 )
400 )
400
401
401 allow_credentials = Bool(False, config=True,
402 allow_credentials = Bool(False, config=True,
402 help="Set the Access-Control-Allow-Credentials: true header"
403 help="Set the Access-Control-Allow-Credentials: true header"
403 )
404 )
404
405
405 default_url = Unicode('/tree', config=True,
406 default_url = Unicode('/tree', config=True,
406 help="The default URL to redirect to from `/`"
407 help="The default URL to redirect to from `/`"
407 )
408 )
408
409
409 ip = Unicode('localhost', config=True,
410 ip = Unicode('localhost', config=True,
410 help="The IP address the notebook server will listen on."
411 help="The IP address the notebook server will listen on."
411 )
412 )
412 def _ip_default(self):
413 def _ip_default(self):
413 """Return localhost if available, 127.0.0.1 otherwise.
414 """Return localhost if available, 127.0.0.1 otherwise.
414
415
415 On some (horribly broken) systems, localhost cannot be bound.
416 On some (horribly broken) systems, localhost cannot be bound.
416 """
417 """
417 s = socket.socket()
418 s = socket.socket()
418 try:
419 try:
419 s.bind(('localhost', 0))
420 s.bind(('localhost', 0))
420 except socket.error as e:
421 except socket.error as e:
421 self.log.warn("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s", e)
422 self.log.warn("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s", e)
422 return '127.0.0.1'
423 return '127.0.0.1'
423 else:
424 else:
424 s.close()
425 s.close()
425 return 'localhost'
426 return 'localhost'
426
427
427 def _ip_changed(self, name, old, new):
428 def _ip_changed(self, name, old, new):
428 if new == u'*': self.ip = u''
429 if new == u'*': self.ip = u''
429
430
430 port = Integer(8888, config=True,
431 port = Integer(8888, config=True,
431 help="The port the notebook server will listen on."
432 help="The port the notebook server will listen on."
432 )
433 )
433 port_retries = Integer(50, config=True,
434 port_retries = Integer(50, config=True,
434 help="The number of additional ports to try if the specified port is not available."
435 help="The number of additional ports to try if the specified port is not available."
435 )
436 )
436
437
437 certfile = Unicode(u'', config=True,
438 certfile = Unicode(u'', config=True,
438 help="""The full path to an SSL/TLS certificate file."""
439 help="""The full path to an SSL/TLS certificate file."""
439 )
440 )
440
441
441 keyfile = Unicode(u'', config=True,
442 keyfile = Unicode(u'', config=True,
442 help="""The full path to a private key file for usage with SSL/TLS."""
443 help="""The full path to a private key file for usage with SSL/TLS."""
443 )
444 )
444
445
445 cookie_secret_file = Unicode(config=True,
446 cookie_secret_file = Unicode(config=True,
446 help="""The file where the cookie secret is stored."""
447 help="""The file where the cookie secret is stored."""
447 )
448 )
448 def _cookie_secret_file_default(self):
449 def _cookie_secret_file_default(self):
449 if self.profile_dir is None:
450 if self.profile_dir is None:
450 return ''
451 return ''
451 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
452 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
452
453
453 cookie_secret = Bytes(b'', config=True,
454 cookie_secret = Bytes(b'', config=True,
454 help="""The random bytes used to secure cookies.
455 help="""The random bytes used to secure cookies.
455 By default this is a new random number every time you start the Notebook.
456 By default this is a new random number every time you start the Notebook.
456 Set it to a value in a config file to enable logins to persist across server sessions.
457 Set it to a value in a config file to enable logins to persist across server sessions.
457
458
458 Note: Cookie secrets should be kept private, do not share config files with
459 Note: Cookie secrets should be kept private, do not share config files with
459 cookie_secret stored in plaintext (you can read the value from a file).
460 cookie_secret stored in plaintext (you can read the value from a file).
460 """
461 """
461 )
462 )
462 def _cookie_secret_default(self):
463 def _cookie_secret_default(self):
463 if os.path.exists(self.cookie_secret_file):
464 if os.path.exists(self.cookie_secret_file):
464 with io.open(self.cookie_secret_file, 'rb') as f:
465 with io.open(self.cookie_secret_file, 'rb') as f:
465 return f.read()
466 return f.read()
466 else:
467 else:
467 secret = base64.encodestring(os.urandom(1024))
468 secret = base64.encodestring(os.urandom(1024))
468 self._write_cookie_secret_file(secret)
469 self._write_cookie_secret_file(secret)
469 return secret
470 return secret
470
471
471 def _write_cookie_secret_file(self, secret):
472 def _write_cookie_secret_file(self, secret):
472 """write my secret to my secret_file"""
473 """write my secret to my secret_file"""
473 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
474 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
474 with io.open(self.cookie_secret_file, 'wb') as f:
475 with io.open(self.cookie_secret_file, 'wb') as f:
475 f.write(secret)
476 f.write(secret)
476 try:
477 try:
477 os.chmod(self.cookie_secret_file, 0o600)
478 os.chmod(self.cookie_secret_file, 0o600)
478 except OSError:
479 except OSError:
479 self.log.warn(
480 self.log.warn(
480 "Could not set permissions on %s",
481 "Could not set permissions on %s",
481 self.cookie_secret_file
482 self.cookie_secret_file
482 )
483 )
483
484
484 password = Unicode(u'', config=True,
485 password = Unicode(u'', config=True,
485 help="""Hashed password to use for web authentication.
486 help="""Hashed password to use for web authentication.
486
487
487 To generate, type in a python/IPython shell:
488 To generate, type in a python/IPython shell:
488
489
489 from IPython.lib import passwd; passwd()
490 from IPython.lib import passwd; passwd()
490
491
491 The string should be of the form type:salt:hashed-password.
492 The string should be of the form type:salt:hashed-password.
492 """
493 """
493 )
494 )
494
495
495 open_browser = Bool(True, config=True,
496 open_browser = Bool(True, config=True,
496 help="""Whether to open in a browser after starting.
497 help="""Whether to open in a browser after starting.
497 The specific browser used is platform dependent and
498 The specific browser used is platform dependent and
498 determined by the python standard library `webbrowser`
499 determined by the python standard library `webbrowser`
499 module, unless it is overridden using the --browser
500 module, unless it is overridden using the --browser
500 (NotebookApp.browser) configuration option.
501 (NotebookApp.browser) configuration option.
501 """)
502 """)
502
503
503 browser = Unicode(u'', config=True,
504 browser = Unicode(u'', config=True,
504 help="""Specify what command to use to invoke a web
505 help="""Specify what command to use to invoke a web
505 browser when opening the notebook. If not specified, the
506 browser when opening the notebook. If not specified, the
506 default browser will be determined by the `webbrowser`
507 default browser will be determined by the `webbrowser`
507 standard library module, which allows setting of the
508 standard library module, which allows setting of the
508 BROWSER environment variable to override it.
509 BROWSER environment variable to override it.
509 """)
510 """)
510
511
511 webapp_settings = Dict(config=True,
512 webapp_settings = Dict(config=True,
512 help="DEPRECATED, use tornado_settings"
513 help="DEPRECATED, use tornado_settings"
513 )
514 )
514 def _webapp_settings_changed(self, name, old, new):
515 def _webapp_settings_changed(self, name, old, new):
515 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
516 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
516 self.tornado_settings = new
517 self.tornado_settings = new
517
518
518 tornado_settings = Dict(config=True,
519 tornado_settings = Dict(config=True,
519 help="Supply overrides for the tornado.web.Application that the "
520 help="Supply overrides for the tornado.web.Application that the "
520 "IPython notebook uses.")
521 "IPython notebook uses.")
521
522
522 ssl_options = Dict(config=True,
523 ssl_options = Dict(config=True,
523 help="""Supply SSL options for the tornado HTTPServer.
524 help="""Supply SSL options for the tornado HTTPServer.
524 See the tornado docs for details.""")
525 See the tornado docs for details.""")
525
526
526 jinja_environment_options = Dict(config=True,
527 jinja_environment_options = Dict(config=True,
527 help="Supply extra arguments that will be passed to Jinja environment.")
528 help="Supply extra arguments that will be passed to Jinja environment.")
529
530 jinja_template_vars = Dict(
531 config=True,
532 help="Extra variables to supply to jinja templates when rendering.",
533 )
528
534
529 enable_mathjax = Bool(True, config=True,
535 enable_mathjax = Bool(True, config=True,
530 help="""Whether to enable MathJax for typesetting math/TeX
536 help="""Whether to enable MathJax for typesetting math/TeX
531
537
532 MathJax is the javascript library IPython uses to render math/LaTeX. It is
538 MathJax is the javascript library IPython uses to render math/LaTeX. It is
533 very large, so you may want to disable it if you have a slow internet
539 very large, so you may want to disable it if you have a slow internet
534 connection, or for offline use of the notebook.
540 connection, or for offline use of the notebook.
535
541
536 When disabled, equations etc. will appear as their untransformed TeX source.
542 When disabled, equations etc. will appear as their untransformed TeX source.
537 """
543 """
538 )
544 )
539 def _enable_mathjax_changed(self, name, old, new):
545 def _enable_mathjax_changed(self, name, old, new):
540 """set mathjax url to empty if mathjax is disabled"""
546 """set mathjax url to empty if mathjax is disabled"""
541 if not new:
547 if not new:
542 self.mathjax_url = u''
548 self.mathjax_url = u''
543
549
544 base_url = Unicode('/', config=True,
550 base_url = Unicode('/', config=True,
545 help='''The base URL for the notebook server.
551 help='''The base URL for the notebook server.
546
552
547 Leading and trailing slashes can be omitted,
553 Leading and trailing slashes can be omitted,
548 and will automatically be added.
554 and will automatically be added.
549 ''')
555 ''')
550 def _base_url_changed(self, name, old, new):
556 def _base_url_changed(self, name, old, new):
551 if not new.startswith('/'):
557 if not new.startswith('/'):
552 self.base_url = '/'+new
558 self.base_url = '/'+new
553 elif not new.endswith('/'):
559 elif not new.endswith('/'):
554 self.base_url = new+'/'
560 self.base_url = new+'/'
555
561
556 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
562 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
557 def _base_project_url_changed(self, name, old, new):
563 def _base_project_url_changed(self, name, old, new):
558 self.log.warn("base_project_url is deprecated, use base_url")
564 self.log.warn("base_project_url is deprecated, use base_url")
559 self.base_url = new
565 self.base_url = new
560
566
561 extra_static_paths = List(Unicode, config=True,
567 extra_static_paths = List(Unicode, config=True,
562 help="""Extra paths to search for serving static files.
568 help="""Extra paths to search for serving static files.
563
569
564 This allows adding javascript/css to be available from the notebook server machine,
570 This allows adding javascript/css to be available from the notebook server machine,
565 or overriding individual files in the IPython"""
571 or overriding individual files in the IPython"""
566 )
572 )
567 def _extra_static_paths_default(self):
573 def _extra_static_paths_default(self):
568 return [os.path.join(self.profile_dir.location, 'static')]
574 return [os.path.join(self.profile_dir.location, 'static')]
569
575
570 @property
576 @property
571 def static_file_path(self):
577 def static_file_path(self):
572 """return extra paths + the default location"""
578 """return extra paths + the default location"""
573 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
579 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
574
580
575 extra_template_paths = List(Unicode, config=True,
581 extra_template_paths = List(Unicode, config=True,
576 help="""Extra paths to search for serving jinja templates.
582 help="""Extra paths to search for serving jinja templates.
577
583
578 Can be used to override templates from IPython.html.templates."""
584 Can be used to override templates from IPython.html.templates."""
579 )
585 )
580 def _extra_template_paths_default(self):
586 def _extra_template_paths_default(self):
581 return []
587 return []
582
588
583 @property
589 @property
584 def template_file_path(self):
590 def template_file_path(self):
585 """return extra paths + the default locations"""
591 """return extra paths + the default locations"""
586 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
592 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
587
593
588 extra_nbextensions_path = List(Unicode, config=True,
594 extra_nbextensions_path = List(Unicode, config=True,
589 help="""extra paths to look for Javascript notebook extensions"""
595 help="""extra paths to look for Javascript notebook extensions"""
590 )
596 )
591
597
592 @property
598 @property
593 def nbextensions_path(self):
599 def nbextensions_path(self):
594 """The path to look for Javascript notebook extensions"""
600 """The path to look for Javascript notebook extensions"""
595 return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS
601 return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS
596
602
597 websocket_url = Unicode("", config=True,
603 websocket_url = Unicode("", config=True,
598 help="""The base URL for websockets,
604 help="""The base URL for websockets,
599 if it differs from the HTTP server (hint: it almost certainly doesn't).
605 if it differs from the HTTP server (hint: it almost certainly doesn't).
600
606
601 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
607 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
602 """
608 """
603 )
609 )
604 mathjax_url = Unicode("", config=True,
610 mathjax_url = Unicode("", config=True,
605 help="""The url for MathJax.js."""
611 help="""The url for MathJax.js."""
606 )
612 )
607 def _mathjax_url_default(self):
613 def _mathjax_url_default(self):
608 if not self.enable_mathjax:
614 if not self.enable_mathjax:
609 return u''
615 return u''
610 static_url_prefix = self.tornado_settings.get("static_url_prefix",
616 static_url_prefix = self.tornado_settings.get("static_url_prefix",
611 url_path_join(self.base_url, "static")
617 url_path_join(self.base_url, "static")
612 )
618 )
613
619
614 # try local mathjax, either in nbextensions/mathjax or static/mathjax
620 # try local mathjax, either in nbextensions/mathjax or static/mathjax
615 for (url_prefix, search_path) in [
621 for (url_prefix, search_path) in [
616 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
622 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
617 (static_url_prefix, self.static_file_path),
623 (static_url_prefix, self.static_file_path),
618 ]:
624 ]:
619 self.log.debug("searching for local mathjax in %s", search_path)
625 self.log.debug("searching for local mathjax in %s", search_path)
620 try:
626 try:
621 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
627 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
622 except IOError:
628 except IOError:
623 continue
629 continue
624 else:
630 else:
625 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
631 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
626 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
632 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
627 return url
633 return url
628
634
629 # no local mathjax, serve from CDN
635 # no local mathjax, serve from CDN
630 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
636 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
631 self.log.info("Using MathJax from CDN: %s", url)
637 self.log.info("Using MathJax from CDN: %s", url)
632 return url
638 return url
633
639
634 def _mathjax_url_changed(self, name, old, new):
640 def _mathjax_url_changed(self, name, old, new):
635 if new and not self.enable_mathjax:
641 if new and not self.enable_mathjax:
636 # enable_mathjax=False overrides mathjax_url
642 # enable_mathjax=False overrides mathjax_url
637 self.mathjax_url = u''
643 self.mathjax_url = u''
638 else:
644 else:
639 self.log.info("Using MathJax: %s", new)
645 self.log.info("Using MathJax: %s", new)
640
646
641 contents_manager_class = Type(
647 contents_manager_class = Type(
642 default_value=FileContentsManager,
648 default_value=FileContentsManager,
643 klass=ContentsManager,
649 klass=ContentsManager,
644 config=True,
650 config=True,
645 help='The notebook manager class to use.'
651 help='The notebook manager class to use.'
646 )
652 )
647 kernel_manager_class = Type(
653 kernel_manager_class = Type(
648 default_value=MappingKernelManager,
654 default_value=MappingKernelManager,
649 config=True,
655 config=True,
650 help='The kernel manager class to use.'
656 help='The kernel manager class to use.'
651 )
657 )
652 session_manager_class = Type(
658 session_manager_class = Type(
653 default_value=SessionManager,
659 default_value=SessionManager,
654 config=True,
660 config=True,
655 help='The session manager class to use.'
661 help='The session manager class to use.'
656 )
662 )
657 cluster_manager_class = Type(
663 cluster_manager_class = Type(
658 default_value=ClusterManager,
664 default_value=ClusterManager,
659 config=True,
665 config=True,
660 help='The cluster manager class to use.'
666 help='The cluster manager class to use.'
661 )
667 )
662
668
663 config_manager_class = Type(
669 config_manager_class = Type(
664 default_value=ConfigManager,
670 default_value=ConfigManager,
665 config = True,
671 config = True,
666 help='The config manager class to use'
672 help='The config manager class to use'
667 )
673 )
668
674
669 kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
675 kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
670
676
671 kernel_spec_manager_class = Type(
677 kernel_spec_manager_class = Type(
672 default_value=KernelSpecManager,
678 default_value=KernelSpecManager,
673 config=True,
679 config=True,
674 help="""
680 help="""
675 The kernel spec manager class to use. Should be a subclass
681 The kernel spec manager class to use. Should be a subclass
676 of `IPython.kernel.kernelspec.KernelSpecManager`.
682 of `IPython.kernel.kernelspec.KernelSpecManager`.
677
683
678 The Api of KernelSpecManager is provisional and might change
684 The Api of KernelSpecManager is provisional and might change
679 without warning between this version of IPython and the next stable one.
685 without warning between this version of IPython and the next stable one.
680 """
686 """
681 )
687 )
682
688
683 login_handler_class = Type(
689 login_handler_class = Type(
684 default_value=LoginHandler,
690 default_value=LoginHandler,
685 klass=web.RequestHandler,
691 klass=web.RequestHandler,
686 config=True,
692 config=True,
687 help='The login handler class to use.',
693 help='The login handler class to use.',
688 )
694 )
689
695
690 logout_handler_class = Type(
696 logout_handler_class = Type(
691 default_value=LogoutHandler,
697 default_value=LogoutHandler,
692 klass=web.RequestHandler,
698 klass=web.RequestHandler,
693 config=True,
699 config=True,
694 help='The logout handler class to use.',
700 help='The logout handler class to use.',
695 )
701 )
696
702
697 trust_xheaders = Bool(False, config=True,
703 trust_xheaders = Bool(False, config=True,
698 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
704 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
699 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
705 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
700 )
706 )
701
707
702 info_file = Unicode()
708 info_file = Unicode()
703
709
704 def _info_file_default(self):
710 def _info_file_default(self):
705 info_file = "nbserver-%s.json"%os.getpid()
711 info_file = "nbserver-%s.json"%os.getpid()
706 return os.path.join(self.profile_dir.security_dir, info_file)
712 return os.path.join(self.profile_dir.security_dir, info_file)
707
713
708 pylab = Unicode('disabled', config=True,
714 pylab = Unicode('disabled', config=True,
709 help="""
715 help="""
710 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
716 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
711 """
717 """
712 )
718 )
713 def _pylab_changed(self, name, old, new):
719 def _pylab_changed(self, name, old, new):
714 """when --pylab is specified, display a warning and exit"""
720 """when --pylab is specified, display a warning and exit"""
715 if new != 'warn':
721 if new != 'warn':
716 backend = ' %s' % new
722 backend = ' %s' % new
717 else:
723 else:
718 backend = ''
724 backend = ''
719 self.log.error("Support for specifying --pylab on the command line has been removed.")
725 self.log.error("Support for specifying --pylab on the command line has been removed.")
720 self.log.error(
726 self.log.error(
721 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
727 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
722 )
728 )
723 self.exit(1)
729 self.exit(1)
724
730
725 notebook_dir = Unicode(config=True,
731 notebook_dir = Unicode(config=True,
726 help="The directory to use for notebooks and kernels."
732 help="The directory to use for notebooks and kernels."
727 )
733 )
728
734
729 def _notebook_dir_default(self):
735 def _notebook_dir_default(self):
730 if self.file_to_run:
736 if self.file_to_run:
731 return os.path.dirname(os.path.abspath(self.file_to_run))
737 return os.path.dirname(os.path.abspath(self.file_to_run))
732 else:
738 else:
733 return py3compat.getcwd()
739 return py3compat.getcwd()
734
740
735 def _notebook_dir_changed(self, name, old, new):
741 def _notebook_dir_changed(self, name, old, new):
736 """Do a bit of validation of the notebook dir."""
742 """Do a bit of validation of the notebook dir."""
737 if not os.path.isabs(new):
743 if not os.path.isabs(new):
738 # If we receive a non-absolute path, make it absolute.
744 # If we receive a non-absolute path, make it absolute.
739 self.notebook_dir = os.path.abspath(new)
745 self.notebook_dir = os.path.abspath(new)
740 return
746 return
741 if not os.path.isdir(new):
747 if not os.path.isdir(new):
742 raise TraitError("No such notebook dir: %r" % new)
748 raise TraitError("No such notebook dir: %r" % new)
743
749
744 # setting App.notebook_dir implies setting notebook and kernel dirs as well
750 # setting App.notebook_dir implies setting notebook and kernel dirs as well
745 self.config.FileContentsManager.root_dir = new
751 self.config.FileContentsManager.root_dir = new
746 self.config.MappingKernelManager.root_dir = new
752 self.config.MappingKernelManager.root_dir = new
747
753
748 server_extensions = List(Unicode(), config=True,
754 server_extensions = List(Unicode(), config=True,
749 help=("Python modules to load as notebook server extensions. "
755 help=("Python modules to load as notebook server extensions. "
750 "This is an experimental API, and may change in future releases.")
756 "This is an experimental API, and may change in future releases.")
751 )
757 )
752
758
753 reraise_server_extension_failures = Bool(
759 reraise_server_extension_failures = Bool(
754 False,
760 False,
755 config=True,
761 config=True,
756 help="Reraise exceptions encountered loading server extensions?",
762 help="Reraise exceptions encountered loading server extensions?",
757 )
763 )
758
764
759 def parse_command_line(self, argv=None):
765 def parse_command_line(self, argv=None):
760 super(NotebookApp, self).parse_command_line(argv)
766 super(NotebookApp, self).parse_command_line(argv)
761
767
762 if self.extra_args:
768 if self.extra_args:
763 arg0 = self.extra_args[0]
769 arg0 = self.extra_args[0]
764 f = os.path.abspath(arg0)
770 f = os.path.abspath(arg0)
765 self.argv.remove(arg0)
771 self.argv.remove(arg0)
766 if not os.path.exists(f):
772 if not os.path.exists(f):
767 self.log.critical("No such file or directory: %s", f)
773 self.log.critical("No such file or directory: %s", f)
768 self.exit(1)
774 self.exit(1)
769
775
770 # Use config here, to ensure that it takes higher priority than
776 # Use config here, to ensure that it takes higher priority than
771 # anything that comes from the profile.
777 # anything that comes from the profile.
772 c = Config()
778 c = Config()
773 if os.path.isdir(f):
779 if os.path.isdir(f):
774 c.NotebookApp.notebook_dir = f
780 c.NotebookApp.notebook_dir = f
775 elif os.path.isfile(f):
781 elif os.path.isfile(f):
776 c.NotebookApp.file_to_run = f
782 c.NotebookApp.file_to_run = f
777 self.update_config(c)
783 self.update_config(c)
778
784
779 def init_configurables(self):
785 def init_configurables(self):
780 self.kernel_spec_manager = self.kernel_spec_manager_class(
786 self.kernel_spec_manager = self.kernel_spec_manager_class(
781 parent=self,
787 parent=self,
782 ipython_dir=self.ipython_dir,
788 ipython_dir=self.ipython_dir,
783 )
789 )
784 self.kernel_manager = self.kernel_manager_class(
790 self.kernel_manager = self.kernel_manager_class(
785 parent=self,
791 parent=self,
786 log=self.log,
792 log=self.log,
787 connection_dir=self.profile_dir.security_dir,
793 connection_dir=self.profile_dir.security_dir,
788 )
794 )
789 self.contents_manager = self.contents_manager_class(
795 self.contents_manager = self.contents_manager_class(
790 parent=self,
796 parent=self,
791 log=self.log,
797 log=self.log,
792 )
798 )
793 self.session_manager = self.session_manager_class(
799 self.session_manager = self.session_manager_class(
794 parent=self,
800 parent=self,
795 log=self.log,
801 log=self.log,
796 kernel_manager=self.kernel_manager,
802 kernel_manager=self.kernel_manager,
797 contents_manager=self.contents_manager,
803 contents_manager=self.contents_manager,
798 )
804 )
799 self.cluster_manager = self.cluster_manager_class(
805 self.cluster_manager = self.cluster_manager_class(
800 parent=self,
806 parent=self,
801 log=self.log,
807 log=self.log,
802 )
808 )
803
809
804 self.config_manager = self.config_manager_class(
810 self.config_manager = self.config_manager_class(
805 parent=self,
811 parent=self,
806 log=self.log,
812 log=self.log,
807 profile_dir=self.profile_dir.location,
813 profile_dir=self.profile_dir.location,
808 )
814 )
809
815
810 def init_logging(self):
816 def init_logging(self):
811 # This prevents double log messages because tornado use a root logger that
817 # This prevents double log messages because tornado use a root logger that
812 # self.log is a child of. The logging module dipatches log messages to a log
818 # self.log is a child of. The logging module dipatches log messages to a log
813 # and all of its ancenstors until propagate is set to False.
819 # and all of its ancenstors until propagate is set to False.
814 self.log.propagate = False
820 self.log.propagate = False
815
821
816 for log in app_log, access_log, gen_log:
822 for log in app_log, access_log, gen_log:
817 # consistent log output name (NotebookApp instead of tornado.access, etc.)
823 # consistent log output name (NotebookApp instead of tornado.access, etc.)
818 log.name = self.log.name
824 log.name = self.log.name
819 # hook up tornado 3's loggers to our app handlers
825 # hook up tornado 3's loggers to our app handlers
820 logger = logging.getLogger('tornado')
826 logger = logging.getLogger('tornado')
821 logger.propagate = True
827 logger.propagate = True
822 logger.parent = self.log
828 logger.parent = self.log
823 logger.setLevel(self.log.level)
829 logger.setLevel(self.log.level)
824
830
825 def init_webapp(self):
831 def init_webapp(self):
826 """initialize tornado webapp and httpserver"""
832 """initialize tornado webapp and httpserver"""
827 self.tornado_settings['allow_origin'] = self.allow_origin
833 self.tornado_settings['allow_origin'] = self.allow_origin
828 if self.allow_origin_pat:
834 if self.allow_origin_pat:
829 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
835 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
830 self.tornado_settings['allow_credentials'] = self.allow_credentials
836 self.tornado_settings['allow_credentials'] = self.allow_credentials
831 # ensure default_url starts with base_url
837 # ensure default_url starts with base_url
832 if not self.default_url.startswith(self.base_url):
838 if not self.default_url.startswith(self.base_url):
833 self.default_url = url_path_join(self.base_url, self.default_url)
839 self.default_url = url_path_join(self.base_url, self.default_url)
834
840
835 self.web_app = NotebookWebApplication(
841 self.web_app = NotebookWebApplication(
836 self, self.kernel_manager, self.contents_manager,
842 self, self.kernel_manager, self.contents_manager,
837 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
843 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
838 self.config_manager,
844 self.config_manager,
839 self.log, self.base_url, self.default_url, self.tornado_settings,
845 self.log, self.base_url, self.default_url, self.tornado_settings,
840 self.jinja_environment_options
846 self.jinja_environment_options
841 )
847 )
842 ssl_options = self.ssl_options
848 ssl_options = self.ssl_options
843 if self.certfile:
849 if self.certfile:
844 ssl_options['certfile'] = self.certfile
850 ssl_options['certfile'] = self.certfile
845 if self.keyfile:
851 if self.keyfile:
846 ssl_options['keyfile'] = self.keyfile
852 ssl_options['keyfile'] = self.keyfile
847 if not ssl_options:
853 if not ssl_options:
848 # None indicates no SSL config
854 # None indicates no SSL config
849 ssl_options = None
855 ssl_options = None
850 else:
856 else:
851 # Disable SSLv3, since its use is discouraged.
857 # Disable SSLv3, since its use is discouraged.
852 ssl_options['ssl_version']=ssl.PROTOCOL_TLSv1
858 ssl_options['ssl_version']=ssl.PROTOCOL_TLSv1
853 self.login_handler_class.validate_security(self, ssl_options=ssl_options)
859 self.login_handler_class.validate_security(self, ssl_options=ssl_options)
854 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
860 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
855 xheaders=self.trust_xheaders)
861 xheaders=self.trust_xheaders)
856
862
857 success = None
863 success = None
858 for port in random_ports(self.port, self.port_retries+1):
864 for port in random_ports(self.port, self.port_retries+1):
859 try:
865 try:
860 self.http_server.listen(port, self.ip)
866 self.http_server.listen(port, self.ip)
861 except socket.error as e:
867 except socket.error as e:
862 if e.errno == errno.EADDRINUSE:
868 if e.errno == errno.EADDRINUSE:
863 self.log.info('The port %i is already in use, trying another random port.' % port)
869 self.log.info('The port %i is already in use, trying another random port.' % port)
864 continue
870 continue
865 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
871 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
866 self.log.warn("Permission to listen on port %i denied" % port)
872 self.log.warn("Permission to listen on port %i denied" % port)
867 continue
873 continue
868 else:
874 else:
869 raise
875 raise
870 else:
876 else:
871 self.port = port
877 self.port = port
872 success = True
878 success = True
873 break
879 break
874 if not success:
880 if not success:
875 self.log.critical('ERROR: the notebook server could not be started because '
881 self.log.critical('ERROR: the notebook server could not be started because '
876 'no available port could be found.')
882 'no available port could be found.')
877 self.exit(1)
883 self.exit(1)
878
884
879 @property
885 @property
880 def display_url(self):
886 def display_url(self):
881 ip = self.ip if self.ip else '[all ip addresses on your system]'
887 ip = self.ip if self.ip else '[all ip addresses on your system]'
882 return self._url(ip)
888 return self._url(ip)
883
889
884 @property
890 @property
885 def connection_url(self):
891 def connection_url(self):
886 ip = self.ip if self.ip else 'localhost'
892 ip = self.ip if self.ip else 'localhost'
887 return self._url(ip)
893 return self._url(ip)
888
894
889 def _url(self, ip):
895 def _url(self, ip):
890 proto = 'https' if self.certfile else 'http'
896 proto = 'https' if self.certfile else 'http'
891 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
897 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
892
898
893 def init_terminals(self):
899 def init_terminals(self):
894 try:
900 try:
895 from .terminal import initialize
901 from .terminal import initialize
896 initialize(self.web_app)
902 initialize(self.web_app)
897 self.web_app.settings['terminals_available'] = True
903 self.web_app.settings['terminals_available'] = True
898 except ImportError as e:
904 except ImportError as e:
899 log = self.log.debug if sys.platform == 'win32' else self.log.warn
905 log = self.log.debug if sys.platform == 'win32' else self.log.warn
900 log("Terminals not available (error was %s)", e)
906 log("Terminals not available (error was %s)", e)
901
907
902 def init_signal(self):
908 def init_signal(self):
903 if not sys.platform.startswith('win'):
909 if not sys.platform.startswith('win'):
904 signal.signal(signal.SIGINT, self._handle_sigint)
910 signal.signal(signal.SIGINT, self._handle_sigint)
905 signal.signal(signal.SIGTERM, self._signal_stop)
911 signal.signal(signal.SIGTERM, self._signal_stop)
906 if hasattr(signal, 'SIGUSR1'):
912 if hasattr(signal, 'SIGUSR1'):
907 # Windows doesn't support SIGUSR1
913 # Windows doesn't support SIGUSR1
908 signal.signal(signal.SIGUSR1, self._signal_info)
914 signal.signal(signal.SIGUSR1, self._signal_info)
909 if hasattr(signal, 'SIGINFO'):
915 if hasattr(signal, 'SIGINFO'):
910 # only on BSD-based systems
916 # only on BSD-based systems
911 signal.signal(signal.SIGINFO, self._signal_info)
917 signal.signal(signal.SIGINFO, self._signal_info)
912
918
913 def _handle_sigint(self, sig, frame):
919 def _handle_sigint(self, sig, frame):
914 """SIGINT handler spawns confirmation dialog"""
920 """SIGINT handler spawns confirmation dialog"""
915 # register more forceful signal handler for ^C^C case
921 # register more forceful signal handler for ^C^C case
916 signal.signal(signal.SIGINT, self._signal_stop)
922 signal.signal(signal.SIGINT, self._signal_stop)
917 # request confirmation dialog in bg thread, to avoid
923 # request confirmation dialog in bg thread, to avoid
918 # blocking the App
924 # blocking the App
919 thread = threading.Thread(target=self._confirm_exit)
925 thread = threading.Thread(target=self._confirm_exit)
920 thread.daemon = True
926 thread.daemon = True
921 thread.start()
927 thread.start()
922
928
923 def _restore_sigint_handler(self):
929 def _restore_sigint_handler(self):
924 """callback for restoring original SIGINT handler"""
930 """callback for restoring original SIGINT handler"""
925 signal.signal(signal.SIGINT, self._handle_sigint)
931 signal.signal(signal.SIGINT, self._handle_sigint)
926
932
927 def _confirm_exit(self):
933 def _confirm_exit(self):
928 """confirm shutdown on ^C
934 """confirm shutdown on ^C
929
935
930 A second ^C, or answering 'y' within 5s will cause shutdown,
936 A second ^C, or answering 'y' within 5s will cause shutdown,
931 otherwise original SIGINT handler will be restored.
937 otherwise original SIGINT handler will be restored.
932
938
933 This doesn't work on Windows.
939 This doesn't work on Windows.
934 """
940 """
935 info = self.log.info
941 info = self.log.info
936 info('interrupted')
942 info('interrupted')
937 print(self.notebook_info())
943 print(self.notebook_info())
938 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
944 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
939 sys.stdout.flush()
945 sys.stdout.flush()
940 r,w,x = select.select([sys.stdin], [], [], 5)
946 r,w,x = select.select([sys.stdin], [], [], 5)
941 if r:
947 if r:
942 line = sys.stdin.readline()
948 line = sys.stdin.readline()
943 if line.lower().startswith('y') and 'n' not in line.lower():
949 if line.lower().startswith('y') and 'n' not in line.lower():
944 self.log.critical("Shutdown confirmed")
950 self.log.critical("Shutdown confirmed")
945 ioloop.IOLoop.current().stop()
951 ioloop.IOLoop.current().stop()
946 return
952 return
947 else:
953 else:
948 print("No answer for 5s:", end=' ')
954 print("No answer for 5s:", end=' ')
949 print("resuming operation...")
955 print("resuming operation...")
950 # no answer, or answer is no:
956 # no answer, or answer is no:
951 # set it back to original SIGINT handler
957 # set it back to original SIGINT handler
952 # use IOLoop.add_callback because signal.signal must be called
958 # use IOLoop.add_callback because signal.signal must be called
953 # from main thread
959 # from main thread
954 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
960 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
955
961
956 def _signal_stop(self, sig, frame):
962 def _signal_stop(self, sig, frame):
957 self.log.critical("received signal %s, stopping", sig)
963 self.log.critical("received signal %s, stopping", sig)
958 ioloop.IOLoop.current().stop()
964 ioloop.IOLoop.current().stop()
959
965
960 def _signal_info(self, sig, frame):
966 def _signal_info(self, sig, frame):
961 print(self.notebook_info())
967 print(self.notebook_info())
962
968
963 def init_components(self):
969 def init_components(self):
964 """Check the components submodule, and warn if it's unclean"""
970 """Check the components submodule, and warn if it's unclean"""
965 status = submodule.check_submodule_status()
971 status = submodule.check_submodule_status()
966 if status == 'missing':
972 if status == 'missing':
967 self.log.warn("components submodule missing, running `git submodule update`")
973 self.log.warn("components submodule missing, running `git submodule update`")
968 submodule.update_submodules(submodule.ipython_parent())
974 submodule.update_submodules(submodule.ipython_parent())
969 elif status == 'unclean':
975 elif status == 'unclean':
970 self.log.warn("components submodule unclean, you may see 404s on static/components")
976 self.log.warn("components submodule unclean, you may see 404s on static/components")
971 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
977 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
972
978
973 def init_server_extensions(self):
979 def init_server_extensions(self):
974 """Load any extensions specified by config.
980 """Load any extensions specified by config.
975
981
976 Import the module, then call the load_jupyter_server_extension function,
982 Import the module, then call the load_jupyter_server_extension function,
977 if one exists.
983 if one exists.
978
984
979 The extension API is experimental, and may change in future releases.
985 The extension API is experimental, and may change in future releases.
980 """
986 """
981 for modulename in self.server_extensions:
987 for modulename in self.server_extensions:
982 try:
988 try:
983 mod = importlib.import_module(modulename)
989 mod = importlib.import_module(modulename)
984 func = getattr(mod, 'load_jupyter_server_extension', None)
990 func = getattr(mod, 'load_jupyter_server_extension', None)
985 if func is not None:
991 if func is not None:
986 func(self)
992 func(self)
987 except Exception:
993 except Exception:
988 if self.reraise_server_extension_failures:
994 if self.reraise_server_extension_failures:
989 raise
995 raise
990 self.log.warn("Error loading server extension %s", modulename,
996 self.log.warn("Error loading server extension %s", modulename,
991 exc_info=True)
997 exc_info=True)
992
998
993 @catch_config_error
999 @catch_config_error
994 def initialize(self, argv=None):
1000 def initialize(self, argv=None):
995 super(NotebookApp, self).initialize(argv)
1001 super(NotebookApp, self).initialize(argv)
996 self.init_logging()
1002 self.init_logging()
997 self.init_configurables()
1003 self.init_configurables()
998 self.init_components()
1004 self.init_components()
999 self.init_webapp()
1005 self.init_webapp()
1000 self.init_terminals()
1006 self.init_terminals()
1001 self.init_signal()
1007 self.init_signal()
1002 self.init_server_extensions()
1008 self.init_server_extensions()
1003
1009
1004 def cleanup_kernels(self):
1010 def cleanup_kernels(self):
1005 """Shutdown all kernels.
1011 """Shutdown all kernels.
1006
1012
1007 The kernels will shutdown themselves when this process no longer exists,
1013 The kernels will shutdown themselves when this process no longer exists,
1008 but explicit shutdown allows the KernelManagers to cleanup the connection files.
1014 but explicit shutdown allows the KernelManagers to cleanup the connection files.
1009 """
1015 """
1010 self.log.info('Shutting down kernels')
1016 self.log.info('Shutting down kernels')
1011 self.kernel_manager.shutdown_all()
1017 self.kernel_manager.shutdown_all()
1012
1018
1013 def notebook_info(self):
1019 def notebook_info(self):
1014 "Return the current working directory and the server url information"
1020 "Return the current working directory and the server url information"
1015 info = self.contents_manager.info_string() + "\n"
1021 info = self.contents_manager.info_string() + "\n"
1016 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
1022 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
1017 return info + "The IPython Notebook is running at: %s" % self.display_url
1023 return info + "The IPython Notebook is running at: %s" % self.display_url
1018
1024
1019 def server_info(self):
1025 def server_info(self):
1020 """Return a JSONable dict of information about this server."""
1026 """Return a JSONable dict of information about this server."""
1021 return {'url': self.connection_url,
1027 return {'url': self.connection_url,
1022 'hostname': self.ip if self.ip else 'localhost',
1028 'hostname': self.ip if self.ip else 'localhost',
1023 'port': self.port,
1029 'port': self.port,
1024 'secure': bool(self.certfile),
1030 'secure': bool(self.certfile),
1025 'base_url': self.base_url,
1031 'base_url': self.base_url,
1026 'notebook_dir': os.path.abspath(self.notebook_dir),
1032 'notebook_dir': os.path.abspath(self.notebook_dir),
1027 'pid': os.getpid()
1033 'pid': os.getpid()
1028 }
1034 }
1029
1035
1030 def write_server_info_file(self):
1036 def write_server_info_file(self):
1031 """Write the result of server_info() to the JSON file info_file."""
1037 """Write the result of server_info() to the JSON file info_file."""
1032 with open(self.info_file, 'w') as f:
1038 with open(self.info_file, 'w') as f:
1033 json.dump(self.server_info(), f, indent=2)
1039 json.dump(self.server_info(), f, indent=2)
1034
1040
1035 def remove_server_info_file(self):
1041 def remove_server_info_file(self):
1036 """Remove the nbserver-<pid>.json file created for this server.
1042 """Remove the nbserver-<pid>.json file created for this server.
1037
1043
1038 Ignores the error raised when the file has already been removed.
1044 Ignores the error raised when the file has already been removed.
1039 """
1045 """
1040 try:
1046 try:
1041 os.unlink(self.info_file)
1047 os.unlink(self.info_file)
1042 except OSError as e:
1048 except OSError as e:
1043 if e.errno != errno.ENOENT:
1049 if e.errno != errno.ENOENT:
1044 raise
1050 raise
1045
1051
1046 def start(self):
1052 def start(self):
1047 """ Start the IPython Notebook server app, after initialization
1053 """ Start the IPython Notebook server app, after initialization
1048
1054
1049 This method takes no arguments so all configuration and initialization
1055 This method takes no arguments so all configuration and initialization
1050 must be done prior to calling this method."""
1056 must be done prior to calling this method."""
1051 if self.subapp is not None:
1057 if self.subapp is not None:
1052 return self.subapp.start()
1058 return self.subapp.start()
1053
1059
1054 info = self.log.info
1060 info = self.log.info
1055 for line in self.notebook_info().split("\n"):
1061 for line in self.notebook_info().split("\n"):
1056 info(line)
1062 info(line)
1057 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
1063 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
1058
1064
1059 self.write_server_info_file()
1065 self.write_server_info_file()
1060
1066
1061 if self.open_browser or self.file_to_run:
1067 if self.open_browser or self.file_to_run:
1062 try:
1068 try:
1063 browser = webbrowser.get(self.browser or None)
1069 browser = webbrowser.get(self.browser or None)
1064 except webbrowser.Error as e:
1070 except webbrowser.Error as e:
1065 self.log.warn('No web browser found: %s.' % e)
1071 self.log.warn('No web browser found: %s.' % e)
1066 browser = None
1072 browser = None
1067
1073
1068 if self.file_to_run:
1074 if self.file_to_run:
1069 if not os.path.exists(self.file_to_run):
1075 if not os.path.exists(self.file_to_run):
1070 self.log.critical("%s does not exist" % self.file_to_run)
1076 self.log.critical("%s does not exist" % self.file_to_run)
1071 self.exit(1)
1077 self.exit(1)
1072
1078
1073 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1079 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1074 uri = url_path_join('notebooks', *relpath.split(os.sep))
1080 uri = url_path_join('notebooks', *relpath.split(os.sep))
1075 else:
1081 else:
1076 uri = 'tree'
1082 uri = 'tree'
1077 if browser:
1083 if browser:
1078 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1084 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1079 new=2)
1085 new=2)
1080 threading.Thread(target=b).start()
1086 threading.Thread(target=b).start()
1081
1087
1082 self.io_loop = ioloop.IOLoop.current()
1088 self.io_loop = ioloop.IOLoop.current()
1083 if sys.platform.startswith('win'):
1089 if sys.platform.startswith('win'):
1084 # add no-op to wake every 5s
1090 # add no-op to wake every 5s
1085 # to handle signals that may be ignored by the inner loop
1091 # to handle signals that may be ignored by the inner loop
1086 pc = ioloop.PeriodicCallback(lambda : None, 5000)
1092 pc = ioloop.PeriodicCallback(lambda : None, 5000)
1087 pc.start()
1093 pc.start()
1088 try:
1094 try:
1089 self.io_loop.start()
1095 self.io_loop.start()
1090 except KeyboardInterrupt:
1096 except KeyboardInterrupt:
1091 info("Interrupted...")
1097 info("Interrupted...")
1092 finally:
1098 finally:
1093 self.cleanup_kernels()
1099 self.cleanup_kernels()
1094 self.remove_server_info_file()
1100 self.remove_server_info_file()
1095
1101
1096 def stop(self):
1102 def stop(self):
1097 def _stop():
1103 def _stop():
1098 self.http_server.stop()
1104 self.http_server.stop()
1099 self.io_loop.stop()
1105 self.io_loop.stop()
1100 self.io_loop.add_callback(_stop)
1106 self.io_loop.add_callback(_stop)
1101
1107
1102
1108
1103 def list_running_servers(profile='default'):
1109 def list_running_servers(profile='default'):
1104 """Iterate over the server info files of running notebook servers.
1110 """Iterate over the server info files of running notebook servers.
1105
1111
1106 Given a profile name, find nbserver-* files in the security directory of
1112 Given a profile name, find nbserver-* files in the security directory of
1107 that profile, and yield dicts of their information, each one pertaining to
1113 that profile, and yield dicts of their information, each one pertaining to
1108 a currently running notebook server instance.
1114 a currently running notebook server instance.
1109 """
1115 """
1110 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1116 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1111 for file in os.listdir(pd.security_dir):
1117 for file in os.listdir(pd.security_dir):
1112 if file.startswith('nbserver-'):
1118 if file.startswith('nbserver-'):
1113 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1119 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1114 info = json.load(f)
1120 info = json.load(f)
1115
1121
1116 # Simple check whether that process is really still running
1122 # Simple check whether that process is really still running
1117 # Also remove leftover files from IPython 2.x without a pid field
1123 # Also remove leftover files from IPython 2.x without a pid field
1118 if ('pid' in info) and check_pid(info['pid']):
1124 if ('pid' in info) and check_pid(info['pid']):
1119 yield info
1125 yield info
1120 else:
1126 else:
1121 # If the process has died, try to delete its info file
1127 # If the process has died, try to delete its info file
1122 try:
1128 try:
1123 os.unlink(file)
1129 os.unlink(file)
1124 except OSError:
1130 except OSError:
1125 pass # TODO: This should warn or log or something
1131 pass # TODO: This should warn or log or something
1126 #-----------------------------------------------------------------------------
1132 #-----------------------------------------------------------------------------
1127 # Main entry point
1133 # Main entry point
1128 #-----------------------------------------------------------------------------
1134 #-----------------------------------------------------------------------------
1129
1135
1130 launch_new_instance = NotebookApp.launch_instance
1136 launch_new_instance = NotebookApp.launch_instance
1131
1137
@@ -1,328 +1,329 b''
1 {% extends "page.html" %}
1 {% extends "page.html" %}
2
2
3 {% block stylesheet %}
3 {% block stylesheet %}
4
4
5 {% if mathjax_url %}
5 {% if mathjax_url %}
6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 {% endif %}
7 {% endif %}
8 <script type="text/javascript">
8 <script type="text/javascript">
9 // MathJax disabled, set as null to distingish from *missing* MathJax,
9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 // where it will be undefined, and should prompt a dialog later.
10 // where it will be undefined, and should prompt a dialog later.
11 window.mathjax_url = "{{mathjax_url}}";
11 window.mathjax_url = "{{mathjax_url}}";
12 </script>
12 </script>
13
13
14 <link rel="stylesheet" href="{{ static_url("components/bootstrap-tour/build/css/bootstrap-tour.min.css") }}" type="text/css" />
14 <link rel="stylesheet" href="{{ static_url("components/bootstrap-tour/build/css/bootstrap-tour.min.css") }}" type="text/css" />
15 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
15 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
16
16
17 {{super()}}
17 {{super()}}
18
18
19 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
19 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
20 <link rel="stylesheet" href="" id='kernel-css' type="text/css" />
20 <link rel="stylesheet" href="" id='kernel-css' type="text/css" />
21
21
22 {% endblock %}
22 {% endblock %}
23
23
24 {% block bodyclasses %}notebook_app {{super()}}{% endblock %}
24 {% block bodyclasses %}notebook_app {{super()}}{% endblock %}
25
25
26 {% block params %}
26 {% block params %}
27
27
28 {{super()}}
28 data-project="{{project}}"
29 data-project="{{project}}"
29 data-base-url="{{base_url}}"
30 data-base-url="{{base_url}}"
30 data-ws-url="{{ws_url}}"
31 data-ws-url="{{ws_url}}"
31 data-notebook-name="{{notebook_name}}"
32 data-notebook-name="{{notebook_name}}"
32 data-notebook-path="{{notebook_path}}"
33 data-notebook-path="{{notebook_path}}"
33
34
34 {% endblock %}
35 {% endblock %}
35
36
36
37
37 {% block headercontainer %}
38 {% block headercontainer %}
38
39
39
40
40 <span id="save_widget" class="pull-left save_widget">
41 <span id="save_widget" class="pull-left save_widget">
41 <span id="notebook_name" class="filename"></span>
42 <span id="notebook_name" class="filename"></span>
42 <span class="checkpoint_status"></span>
43 <span class="checkpoint_status"></span>
43 <span class="autosave_status"></span>
44 <span class="autosave_status"></span>
44 </span>
45 </span>
45
46
46 <span id="kernel_logo_widget">
47 <span id="kernel_logo_widget">
47 <img class="current_kernel_logo" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
48 <img class="current_kernel_logo" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
48 </span>
49 </span>
49
50
50 {% endblock headercontainer %}
51 {% endblock headercontainer %}
51
52
52 {% block header %}
53 {% block header %}
53 <div id="menubar-container" class="container">
54 <div id="menubar-container" class="container">
54 <div id="menubar">
55 <div id="menubar">
55 <div id="menus" class="navbar navbar-default" role="navigation">
56 <div id="menus" class="navbar navbar-default" role="navigation">
56 <div class="container-fluid">
57 <div class="container-fluid">
57 <button type="button" class="btn btn-default navbar-btn navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
58 <button type="button" class="btn btn-default navbar-btn navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
58 <i class="fa fa-bars"></i>
59 <i class="fa fa-bars"></i>
59 <span class="navbar-text">Menu</span>
60 <span class="navbar-text">Menu</span>
60 </button>
61 </button>
61 <p id="kernel_indicator" class="navbar-text indicator_area">
62 <p id="kernel_indicator" class="navbar-text indicator_area">
62 <span class="kernel_indicator_name">Kernel</span>
63 <span class="kernel_indicator_name">Kernel</span>
63 <i id="kernel_indicator_icon"></i>
64 <i id="kernel_indicator_icon"></i>
64 </p>
65 </p>
65 <i id="readonly-indicator" class="navbar-text" title='This notebook is read-only'>
66 <i id="readonly-indicator" class="navbar-text" title='This notebook is read-only'>
66 <span class="fa-stack">
67 <span class="fa-stack">
67 <i class="fa fa-save fa-stack-1x"></i>
68 <i class="fa fa-save fa-stack-1x"></i>
68 <i class="fa fa-ban fa-stack-2x text-danger"></i>
69 <i class="fa fa-ban fa-stack-2x text-danger"></i>
69 </span>
70 </span>
70 </i>
71 </i>
71 <i id="modal_indicator" class="navbar-text"></i>
72 <i id="modal_indicator" class="navbar-text"></i>
72 <span id="notification_area"></span>
73 <span id="notification_area"></span>
73 <div class="navbar-collapse collapse">
74 <div class="navbar-collapse collapse">
74 <ul class="nav navbar-nav">
75 <ul class="nav navbar-nav">
75 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
76 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
76 <ul id="file_menu" class="dropdown-menu">
77 <ul id="file_menu" class="dropdown-menu">
77 <li id="new_notebook" class="dropdown-submenu">
78 <li id="new_notebook" class="dropdown-submenu">
78 <a href="#">New Notebook</a>
79 <a href="#">New Notebook</a>
79 <ul class="dropdown-menu" id="menu-new-notebook-submenu"></ul>
80 <ul class="dropdown-menu" id="menu-new-notebook-submenu"></ul>
80 </li>
81 </li>
81 <li id="open_notebook"
82 <li id="open_notebook"
82 title="Opens a new window with the Dashboard view">
83 title="Opens a new window with the Dashboard view">
83 <a href="#">Open...</a></li>
84 <a href="#">Open...</a></li>
84 <!-- <hr/> -->
85 <!-- <hr/> -->
85 <li class="divider"></li>
86 <li class="divider"></li>
86 <li id="copy_notebook"
87 <li id="copy_notebook"
87 title="Open a copy of this notebook's contents and start a new kernel">
88 title="Open a copy of this notebook's contents and start a new kernel">
88 <a href="#">Make a Copy...</a></li>
89 <a href="#">Make a Copy...</a></li>
89 <li id="rename_notebook"><a href="#">Rename...</a></li>
90 <li id="rename_notebook"><a href="#">Rename...</a></li>
90 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
91 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
91 <!-- <hr/> -->
92 <!-- <hr/> -->
92 <li class="divider"></li>
93 <li class="divider"></li>
93 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
94 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
94 <ul class="dropdown-menu">
95 <ul class="dropdown-menu">
95 <li><a href="#"></a></li>
96 <li><a href="#"></a></li>
96 <li><a href="#"></a></li>
97 <li><a href="#"></a></li>
97 <li><a href="#"></a></li>
98 <li><a href="#"></a></li>
98 <li><a href="#"></a></li>
99 <li><a href="#"></a></li>
99 <li><a href="#"></a></li>
100 <li><a href="#"></a></li>
100 </ul>
101 </ul>
101 </li>
102 </li>
102 <li class="divider"></li>
103 <li class="divider"></li>
103 <li id="print_preview"><a href="#">Print Preview</a></li>
104 <li id="print_preview"><a href="#">Print Preview</a></li>
104 <li class="dropdown-submenu"><a href="#">Download as</a>
105 <li class="dropdown-submenu"><a href="#">Download as</a>
105 <ul class="dropdown-menu">
106 <ul class="dropdown-menu">
106 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
107 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
107 <li id="download_script"><a href="#">Script</a></li>
108 <li id="download_script"><a href="#">Script</a></li>
108 <li id="download_html"><a href="#">HTML (.html)</a></li>
109 <li id="download_html"><a href="#">HTML (.html)</a></li>
109 <li id="download_markdown"><a href="#">Markdown (.md)</a></li>
110 <li id="download_markdown"><a href="#">Markdown (.md)</a></li>
110 <li id="download_rst"><a href="#">reST (.rst)</a></li>
111 <li id="download_rst"><a href="#">reST (.rst)</a></li>
111 <li id="download_pdf"><a href="#">PDF via LaTeX (.pdf)</a></li>
112 <li id="download_pdf"><a href="#">PDF via LaTeX (.pdf)</a></li>
112 </ul>
113 </ul>
113 </li>
114 </li>
114 <li class="divider"></li>
115 <li class="divider"></li>
115 <li id="trust_notebook"
116 <li id="trust_notebook"
116 title="Trust the output of this notebook">
117 title="Trust the output of this notebook">
117 <a href="#" >Trust Notebook</a></li>
118 <a href="#" >Trust Notebook</a></li>
118 <li class="divider"></li>
119 <li class="divider"></li>
119 <li id="kill_and_exit"
120 <li id="kill_and_exit"
120 title="Shutdown this notebook's kernel, and close this window">
121 title="Shutdown this notebook's kernel, and close this window">
121 <a href="#" >Close and Halt</a></li>
122 <a href="#" >Close and Halt</a></li>
122 </ul>
123 </ul>
123 </li>
124 </li>
124 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
125 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
125 <ul id="edit_menu" class="dropdown-menu">
126 <ul id="edit_menu" class="dropdown-menu">
126 <li id="cut_cell"><a href="#">Cut Cell</a></li>
127 <li id="cut_cell"><a href="#">Cut Cell</a></li>
127 <li id="copy_cell"><a href="#">Copy Cell</a></li>
128 <li id="copy_cell"><a href="#">Copy Cell</a></li>
128 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
129 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
129 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
130 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
130 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
131 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
131 <li id="delete_cell"><a href="#">Delete Cell</a></li>
132 <li id="delete_cell"><a href="#">Delete Cell</a></li>
132 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
133 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
133 <li class="divider"></li>
134 <li class="divider"></li>
134 <li id="split_cell"><a href="#">Split Cell</a></li>
135 <li id="split_cell"><a href="#">Split Cell</a></li>
135 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
136 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
136 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
137 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
137 <li class="divider"></li>
138 <li class="divider"></li>
138 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
139 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
139 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
140 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
140 <li class="divider"></li>
141 <li class="divider"></li>
141 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
142 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
142 </ul>
143 </ul>
143 </li>
144 </li>
144 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
145 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
145 <ul id="view_menu" class="dropdown-menu">
146 <ul id="view_menu" class="dropdown-menu">
146 <li id="toggle_header"
147 <li id="toggle_header"
147 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
148 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
148 <a href="#">Toggle Header</a></li>
149 <a href="#">Toggle Header</a></li>
149 <li id="toggle_toolbar"
150 <li id="toggle_toolbar"
150 title="Show/Hide the action icons (below menu bar)">
151 title="Show/Hide the action icons (below menu bar)">
151 <a href="#">Toggle Toolbar</a></li>
152 <a href="#">Toggle Toolbar</a></li>
152 </ul>
153 </ul>
153 </li>
154 </li>
154 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
155 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
155 <ul id="insert_menu" class="dropdown-menu">
156 <ul id="insert_menu" class="dropdown-menu">
156 <li id="insert_cell_above"
157 <li id="insert_cell_above"
157 title="Insert an empty Code cell above the currently active cell">
158 title="Insert an empty Code cell above the currently active cell">
158 <a href="#">Insert Cell Above</a></li>
159 <a href="#">Insert Cell Above</a></li>
159 <li id="insert_cell_below"
160 <li id="insert_cell_below"
160 title="Insert an empty Code cell below the currently active cell">
161 title="Insert an empty Code cell below the currently active cell">
161 <a href="#">Insert Cell Below</a></li>
162 <a href="#">Insert Cell Below</a></li>
162 </ul>
163 </ul>
163 </li>
164 </li>
164 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
165 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
165 <ul id="cell_menu" class="dropdown-menu">
166 <ul id="cell_menu" class="dropdown-menu">
166 <li id="run_cell" title="Run this cell, and move cursor to the next one">
167 <li id="run_cell" title="Run this cell, and move cursor to the next one">
167 <a href="#">Run</a></li>
168 <a href="#">Run</a></li>
168 <li id="run_cell_select_below" title="Run this cell, select below">
169 <li id="run_cell_select_below" title="Run this cell, select below">
169 <a href="#">Run and Select Below</a></li>
170 <a href="#">Run and Select Below</a></li>
170 <li id="run_cell_insert_below" title="Run this cell, insert below">
171 <li id="run_cell_insert_below" title="Run this cell, insert below">
171 <a href="#">Run and Insert Below</a></li>
172 <a href="#">Run and Insert Below</a></li>
172 <li id="run_all_cells" title="Run all cells in the notebook">
173 <li id="run_all_cells" title="Run all cells in the notebook">
173 <a href="#">Run All</a></li>
174 <a href="#">Run All</a></li>
174 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
175 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
175 <a href="#">Run All Above</a></li>
176 <a href="#">Run All Above</a></li>
176 <li id="run_all_cells_below" title="Run this cell and all cells below it">
177 <li id="run_all_cells_below" title="Run this cell and all cells below it">
177 <a href="#">Run All Below</a></li>
178 <a href="#">Run All Below</a></li>
178 <li class="divider"></li>
179 <li class="divider"></li>
179 <li id="change_cell_type" class="dropdown-submenu"
180 <li id="change_cell_type" class="dropdown-submenu"
180 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
181 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
181 <a href="#">Cell Type</a>
182 <a href="#">Cell Type</a>
182 <ul class="dropdown-menu">
183 <ul class="dropdown-menu">
183 <li id="to_code"
184 <li id="to_code"
184 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
185 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
185 <a href="#">Code</a></li>
186 <a href="#">Code</a></li>
186 <li id="to_markdown"
187 <li id="to_markdown"
187 title="Contents will be rendered as HTML and serve as explanatory text">
188 title="Contents will be rendered as HTML and serve as explanatory text">
188 <a href="#">Markdown</a></li>
189 <a href="#">Markdown</a></li>
189 <li id="to_raw"
190 <li id="to_raw"
190 title="Contents will pass through nbconvert unmodified">
191 title="Contents will pass through nbconvert unmodified">
191 <a href="#">Raw NBConvert</a></li>
192 <a href="#">Raw NBConvert</a></li>
192 </ul>
193 </ul>
193 </li>
194 </li>
194 <li class="divider"></li>
195 <li class="divider"></li>
195 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
196 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
196 <ul class="dropdown-menu">
197 <ul class="dropdown-menu">
197 <li id="toggle_current_output"
198 <li id="toggle_current_output"
198 title="Hide/Show the output of the current cell">
199 title="Hide/Show the output of the current cell">
199 <a href="#">Toggle</a>
200 <a href="#">Toggle</a>
200 </li>
201 </li>
201 <li id="toggle_current_output_scroll"
202 <li id="toggle_current_output_scroll"
202 title="Scroll the output of the current cell">
203 title="Scroll the output of the current cell">
203 <a href="#">Toggle Scrolling</a>
204 <a href="#">Toggle Scrolling</a>
204 </li>
205 </li>
205 <li id="clear_current_output"
206 <li id="clear_current_output"
206 title="Clear the output of the current cell">
207 title="Clear the output of the current cell">
207 <a href="#">Clear</a>
208 <a href="#">Clear</a>
208 </li>
209 </li>
209 </ul>
210 </ul>
210 </li>
211 </li>
211 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
212 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
212 <ul class="dropdown-menu">
213 <ul class="dropdown-menu">
213 <li id="toggle_all_output"
214 <li id="toggle_all_output"
214 title="Hide/Show the output of all cells">
215 title="Hide/Show the output of all cells">
215 <a href="#">Toggle</a>
216 <a href="#">Toggle</a>
216 </li>
217 </li>
217 <li id="toggle_all_output_scroll"
218 <li id="toggle_all_output_scroll"
218 title="Scroll the output of all cells">
219 title="Scroll the output of all cells">
219 <a href="#">Toggle Scrolling</a>
220 <a href="#">Toggle Scrolling</a>
220 </li>
221 </li>
221 <li id="clear_all_output"
222 <li id="clear_all_output"
222 title="Clear the output of all cells">
223 title="Clear the output of all cells">
223 <a href="#">Clear</a>
224 <a href="#">Clear</a>
224 </li>
225 </li>
225 </ul>
226 </ul>
226 </li>
227 </li>
227 </ul>
228 </ul>
228 </li>
229 </li>
229 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
230 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
230 <ul id="kernel_menu" class="dropdown-menu">
231 <ul id="kernel_menu" class="dropdown-menu">
231 <li id="int_kernel"
232 <li id="int_kernel"
232 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
233 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
233 <a href="#">Interrupt</a>
234 <a href="#">Interrupt</a>
234 </li>
235 </li>
235 <li id="restart_kernel"
236 <li id="restart_kernel"
236 title="Restart the Kernel">
237 title="Restart the Kernel">
237 <a href="#">Restart</a>
238 <a href="#">Restart</a>
238 </li>
239 </li>
239 <li id="reconnect_kernel"
240 <li id="reconnect_kernel"
240 title="Reconnect to the Kernel">
241 title="Reconnect to the Kernel">
241 <a href="#">Reconnect</a>
242 <a href="#">Reconnect</a>
242 </li>
243 </li>
243 <li class="divider"></li>
244 <li class="divider"></li>
244 <li id="menu-change-kernel" class="dropdown-submenu">
245 <li id="menu-change-kernel" class="dropdown-submenu">
245 <a href="#">Change kernel</a>
246 <a href="#">Change kernel</a>
246 <ul class="dropdown-menu" id="menu-change-kernel-submenu"></ul>
247 <ul class="dropdown-menu" id="menu-change-kernel-submenu"></ul>
247 </li>
248 </li>
248 </ul>
249 </ul>
249 </li>
250 </li>
250 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
251 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
251 <ul id="help_menu" class="dropdown-menu">
252 <ul id="help_menu" class="dropdown-menu">
252 <li id="notebook_tour" title="A quick tour of the notebook user interface"><a href="#">User Interface Tour</a></li>
253 <li id="notebook_tour" title="A quick tour of the notebook user interface"><a href="#">User Interface Tour</a></li>
253 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
254 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
254 <li class="divider"></li>
255 <li class="divider"></li>
255 {% set
256 {% set
256 sections = (
257 sections = (
257 (
258 (
258 ("http://nbviewer.ipython.org/github/ipython/ipython/blob/3.x/examples/Notebook/Index.ipynb", "Notebook Help", True),
259 ("http://nbviewer.ipython.org/github/ipython/ipython/blob/3.x/examples/Notebook/Index.ipynb", "Notebook Help", True),
259 ("https://help.github.com/articles/markdown-basics/","Markdown",True),
260 ("https://help.github.com/articles/markdown-basics/","Markdown",True),
260 ),
261 ),
261 )
262 )
262 %}
263 %}
263
264
264 {% for helplinks in sections %}
265 {% for helplinks in sections %}
265 {% for link in helplinks %}
266 {% for link in helplinks %}
266 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
267 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
267 {{'<i class="fa fa-external-link menu-icon pull-right"></i>' if link[2]}}
268 {{'<i class="fa fa-external-link menu-icon pull-right"></i>' if link[2]}}
268 {{link[1]}}
269 {{link[1]}}
269 </a></li>
270 </a></li>
270 {% endfor %}
271 {% endfor %}
271 {% if not loop.last %}
272 {% if not loop.last %}
272 <li class="divider"></li>
273 <li class="divider"></li>
273 {% endif %}
274 {% endif %}
274 {% endfor %}
275 {% endfor %}
275 <li class="divider"></li>
276 <li class="divider"></li>
276 <li title="About IPython Notebook"><a id="notebook_about" href="#">About</a></li>
277 <li title="About IPython Notebook"><a id="notebook_about" href="#">About</a></li>
277 </ul>
278 </ul>
278 </li>
279 </li>
279 </ul>
280 </ul>
280 </div>
281 </div>
281 </div>
282 </div>
282 </div>
283 </div>
283 </div>
284 </div>
284
285
285 <div id="maintoolbar" class="navbar">
286 <div id="maintoolbar" class="navbar">
286 <div class="toolbar-inner navbar-inner navbar-nobg">
287 <div class="toolbar-inner navbar-inner navbar-nobg">
287 <div id="maintoolbar-container" class="container"></div>
288 <div id="maintoolbar-container" class="container"></div>
288 </div>
289 </div>
289 </div>
290 </div>
290 </div>
291 </div>
291
292
292 <div class="lower-header-bar"></div>
293 <div class="lower-header-bar"></div>
293 {% endblock header %}
294 {% endblock header %}
294
295
295 {% block site %}
296 {% block site %}
296
297
297 <div id="ipython-main-app">
298 <div id="ipython-main-app">
298 <div id="notebook_panel">
299 <div id="notebook_panel">
299 <div id="notebook"></div>
300 <div id="notebook"></div>
300 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
301 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
301 </div>
302 </div>
302 </div>
303 </div>
303
304
304
305
305 {% endblock %}
306 {% endblock %}
306
307
307 {% block after_site %}
308 {% block after_site %}
308
309
309 <div id="pager">
310 <div id="pager">
310 <div id="pager-contents">
311 <div id="pager-contents">
311 <div id="pager-container" class="container"></div>
312 <div id="pager-container" class="container"></div>
312 </div>
313 </div>
313 <div id='pager-button-area'></div>
314 <div id='pager-button-area'></div>
314 </div>
315 </div>
315
316
316 {% endblock %}
317 {% endblock %}
317
318
318 {% block script %}
319 {% block script %}
319 {{super()}}
320 {{super()}}
320 <script type="text/javascript">
321 <script type="text/javascript">
321 sys_info = {{sys_info}};
322 sys_info = {{sys_info}};
322 </script>
323 </script>
323
324
324 <script src="{{ static_url("components/text-encoding/lib/encoding.js") }}" charset="utf-8"></script>
325 <script src="{{ static_url("components/text-encoding/lib/encoding.js") }}" charset="utf-8"></script>
325
326
326 <script src="{{ static_url("notebook/js/main.js") }}" charset="utf-8"></script>
327 <script src="{{ static_url("notebook/js/main.js") }}" charset="utf-8"></script>
327
328
328 {% endblock %}
329 {% endblock %}
@@ -1,187 +1,187 b''
1 {% extends "page.html" %}
1 {% extends "page.html" %}
2
2
3 {% block title %}{{page_title}}{% endblock %}
3 {% block title %}{{page_title}}{% endblock %}
4
4
5
5
6 {% block params %}
6 {% block params %}
7
7 {{super()}}
8 data-base-url="{{base_url}}"
8 data-base-url="{{base_url}}"
9 data-notebook-path="{{notebook_path}}"
9 data-notebook-path="{{notebook_path}}"
10 data-terminals-available="{{terminals_available}}"
10 data-terminals-available="{{terminals_available}}"
11
11
12 {% endblock %}
12 {% endblock %}
13
13
14
14
15 {% block site %}
15 {% block site %}
16
16
17 <div id="ipython-main-app" class="container">
17 <div id="ipython-main-app" class="container">
18 <div id="tab_content" class="tabbable">
18 <div id="tab_content" class="tabbable">
19 <ul id="tabs" class="nav nav-tabs">
19 <ul id="tabs" class="nav nav-tabs">
20 <li class="active"><a href="#notebooks" data-toggle="tab">Files</a></li>
20 <li class="active"><a href="#notebooks" data-toggle="tab">Files</a></li>
21 <li><a href="#running" data-toggle="tab">Running</a></li>
21 <li><a href="#running" data-toggle="tab">Running</a></li>
22 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
22 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
23 </ul>
23 </ul>
24 <div class="tab-content">
24 <div class="tab-content">
25 <div id="notebooks" class="tab-pane active">
25 <div id="notebooks" class="tab-pane active">
26 <div id="notebook_toolbar" class="row">
26 <div id="notebook_toolbar" class="row">
27 <div class="col-sm-8 no-padding">
27 <div class="col-sm-8 no-padding">
28 <div class="dynamic-instructions">
28 <div class="dynamic-instructions">
29 Select items to perform actions on them.
29 Select items to perform actions on them.
30 </div>
30 </div>
31 <div class="dynamic-buttons">
31 <div class="dynamic-buttons">
32 <button title="Duplicate selected" class="duplicate-button btn btn-default btn-xs">Duplicate</button>
32 <button title="Duplicate selected" class="duplicate-button btn btn-default btn-xs">Duplicate</button>
33 <button title="Rename selected" class="rename-button btn btn-default btn-xs">Rename</button>
33 <button title="Rename selected" class="rename-button btn btn-default btn-xs">Rename</button>
34 <button title="Shutdown selected notebook(s)" class="shutdown-button btn btn-default btn-xs btn-warning">Shutdown</button>
34 <button title="Shutdown selected notebook(s)" class="shutdown-button btn btn-default btn-xs btn-warning">Shutdown</button>
35 <button title="Deleted selected" class="delete-button btn btn-default btn-xs btn-danger"><i class="fa fa-trash"></i></button>
35 <button title="Deleted selected" class="delete-button btn btn-default btn-xs btn-danger"><i class="fa fa-trash"></i></button>
36 </div>
36 </div>
37 </div>
37 </div>
38 <div class="col-sm-4 no-padding tree-buttons">
38 <div class="col-sm-4 no-padding tree-buttons">
39 <div class="pull-right">
39 <div class="pull-right">
40 <form id='alternate_upload' class='alternate_upload'>
40 <form id='alternate_upload' class='alternate_upload'>
41 <span id="notebook_list_info">
41 <span id="notebook_list_info">
42 <span class="btn btn-xs btn-default btn-upload">
42 <span class="btn btn-xs btn-default btn-upload">
43 <input title="Click to browse for a file to upload." type="file" name="datafile" class="fileinput" multiple='multiple'>
43 <input title="Click to browse for a file to upload." type="file" name="datafile" class="fileinput" multiple='multiple'>
44 Upload
44 Upload
45 </span>
45 </span>
46 </span>
46 </span>
47 </form>
47 </form>
48 <div id="new-buttons" class="btn-group">
48 <div id="new-buttons" class="btn-group">
49 <button class="dropdown-toggle btn btn-default btn-xs" data-toggle="dropdown">
49 <button class="dropdown-toggle btn btn-default btn-xs" data-toggle="dropdown">
50 <span>New</span>
50 <span>New</span>
51 <span class="caret"></span>
51 <span class="caret"></span>
52 </button>
52 </button>
53 <ul id="new-menu" class="dropdown-menu">
53 <ul id="new-menu" class="dropdown-menu">
54 <li role="presentation" id="new-file">
54 <li role="presentation" id="new-file">
55 <a role="menuitem" tabindex="-1" href="#">Text File</a>
55 <a role="menuitem" tabindex="-1" href="#">Text File</a>
56 </li>
56 </li>
57 <li role="presentation" id="new-folder">
57 <li role="presentation" id="new-folder">
58 <a role="menuitem" tabindex="-1" href="#">Folder</a>
58 <a role="menuitem" tabindex="-1" href="#">Folder</a>
59 </li>
59 </li>
60 {% if terminals_available %}
60 {% if terminals_available %}
61 <li role="presentation" id="new-terminal">
61 <li role="presentation" id="new-terminal">
62 <a role="menuitem" tabindex="-1" href="#">Terminal</a>
62 <a role="menuitem" tabindex="-1" href="#">Terminal</a>
63 </li>
63 </li>
64 {% else %}
64 {% else %}
65 <li role="presentation" id="new-terminal-disabled" class="disabled">
65 <li role="presentation" id="new-terminal-disabled" class="disabled">
66 <a role="menuitem" tabindex="-1" href="#">Terminals Unavailable</a>
66 <a role="menuitem" tabindex="-1" href="#">Terminals Unavailable</a>
67 </li>
67 </li>
68 {% endif %}
68 {% endif %}
69 <li role="presentation" class="divider"></li>
69 <li role="presentation" class="divider"></li>
70 <li role="presentation" class="dropdown-header" id="notebook-kernels">Notebooks</li>
70 <li role="presentation" class="dropdown-header" id="notebook-kernels">Notebooks</li>
71 </ul>
71 </ul>
72 </div>
72 </div>
73 <div class="btn-group">
73 <div class="btn-group">
74 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
74 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
75 </div>
75 </div>
76 </div>
76 </div>
77 </div>
77 </div>
78 </div>
78 </div>
79 <div id="notebook_list">
79 <div id="notebook_list">
80 <div id="notebook_list_header" class="row list_header">
80 <div id="notebook_list_header" class="row list_header">
81 <div class="btn-group dropdown" id="tree-selector">
81 <div class="btn-group dropdown" id="tree-selector">
82 <button title="Select All / None" type="button" class="btn btn-default btn-xs" id="button-select-all">
82 <button title="Select All / None" type="button" class="btn btn-default btn-xs" id="button-select-all">
83 <input type="checkbox" class="pull-left tree-selector" id="select-all"><span id="counter-select-all">&nbsp;</span></input>
83 <input type="checkbox" class="pull-left tree-selector" id="select-all"><span id="counter-select-all">&nbsp;</span></input>
84 </button>
84 </button>
85 <button title="Select..." class="btn btn-default btn-xs dropdown-toggle" type="button" id="tree-selector-btn" data-toggle="dropdown" aria-expanded="true">
85 <button title="Select..." class="btn btn-default btn-xs dropdown-toggle" type="button" id="tree-selector-btn" data-toggle="dropdown" aria-expanded="true">
86 <span class="caret"></span>
86 <span class="caret"></span>
87 <span class="sr-only">Toggle Dropdown</span>
87 <span class="sr-only">Toggle Dropdown</span>
88 </button>
88 </button>
89 <ul id='selector-menu' class="dropdown-menu" role="menu" aria-labelledby="tree-selector-btn">
89 <ul id='selector-menu' class="dropdown-menu" role="menu" aria-labelledby="tree-selector-btn">
90 <li role="presentation"><a id="select-folders" role="menuitem" tabindex="-1" href="#" title="Select All Folders"><i class="menu_icon folder_icon icon-fixed-width"></i> Folders</a></li>
90 <li role="presentation"><a id="select-folders" role="menuitem" tabindex="-1" href="#" title="Select All Folders"><i class="menu_icon folder_icon icon-fixed-width"></i> Folders</a></li>
91 <li role="presentation"><a id="select-notebooks" role="menuitem" tabindex="-1" href="#" title="Select All Notebooks"><i class="menu_icon notebook_icon icon-fixed-width"></i> All Notebooks</a></li>
91 <li role="presentation"><a id="select-notebooks" role="menuitem" tabindex="-1" href="#" title="Select All Notebooks"><i class="menu_icon notebook_icon icon-fixed-width"></i> All Notebooks</a></li>
92 <li role="presentation"><a id="select-running-notebooks" role="menuitem" tabindex="-1" href="#" title="Select Running Notebooks"><i class="menu_icon running_notebook_icon icon-fixed-width"></i> Running</a></li>
92 <li role="presentation"><a id="select-running-notebooks" role="menuitem" tabindex="-1" href="#" title="Select Running Notebooks"><i class="menu_icon running_notebook_icon icon-fixed-width"></i> Running</a></li>
93 <li role="presentation"><a id="select-files" role="menuitem" tabindex="-1" href="#" title="Select All Files"><i class="menu_icon file_icon icon-fixed-width"></i> Files</a></li>
93 <li role="presentation"><a id="select-files" role="menuitem" tabindex="-1" href="#" title="Select All Files"><i class="menu_icon file_icon icon-fixed-width"></i> Files</a></li>
94 </ul>
94 </ul>
95 </div>
95 </div>
96 <div id="project_name">
96 <div id="project_name">
97 <ul class="breadcrumb">
97 <ul class="breadcrumb">
98 <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-home"></i></a></li>
98 <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-home"></i></a></li>
99 {% for crumb in breadcrumbs[1:] %}
99 {% for crumb in breadcrumbs[1:] %}
100 <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
100 <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
101 {% endfor %}
101 {% endfor %}
102 </ul>
102 </ul>
103 </div>
103 </div>
104 </div>
104 </div>
105 </div>
105 </div>
106 </div>
106 </div>
107 <div id="running" class="tab-pane">
107 <div id="running" class="tab-pane">
108 <div id="running_toolbar" class="row">
108 <div id="running_toolbar" class="row">
109 <div class="col-sm-8 no-padding">
109 <div class="col-sm-8 no-padding">
110 <span id="running_list_info">Currently running Jupyter processes</span>
110 <span id="running_list_info">Currently running Jupyter processes</span>
111 </div>
111 </div>
112 <div class="col-sm-4 no-padding tree-buttons">
112 <div class="col-sm-4 no-padding tree-buttons">
113 <span id="running_buttons" class="pull-right">
113 <span id="running_buttons" class="pull-right">
114 <button id="refresh_running_list" title="Refresh running list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
114 <button id="refresh_running_list" title="Refresh running list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
115 </span>
115 </span>
116 </div>
116 </div>
117 </div>
117 </div>
118 <div class="panel-group" id="accordion" >
118 <div class="panel-group" id="accordion" >
119 <div class="panel panel-default">
119 <div class="panel panel-default">
120 <div class="panel-heading">
120 <div class="panel-heading">
121 <a data-toggle="collapse" data-target="#collapseOne" href="#">
121 <a data-toggle="collapse" data-target="#collapseOne" href="#">
122 Terminals
122 Terminals
123 </a>
123 </a>
124 </div>
124 </div>
125 <div id="collapseOne" class=" collapse in">
125 <div id="collapseOne" class=" collapse in">
126 <div class="panel-body">
126 <div class="panel-body">
127 <div id="terminal_list">
127 <div id="terminal_list">
128 <div id="terminal_list_header" class="row list_placeholder">
128 <div id="terminal_list_header" class="row list_placeholder">
129 {% if terminals_available %}
129 {% if terminals_available %}
130 <div> There are no terminals running. </div>
130 <div> There are no terminals running. </div>
131 {% else %}
131 {% else %}
132 <div> Terminals are unavailable. </div>
132 <div> Terminals are unavailable. </div>
133 {% endif %}
133 {% endif %}
134 </div>
134 </div>
135 </div>
135 </div>
136 </div>
136 </div>
137 </div>
137 </div>
138 </div>
138 </div>
139 <div class="panel panel-default">
139 <div class="panel panel-default">
140 <div class="panel-heading">
140 <div class="panel-heading">
141 <a data-toggle="collapse" data-target="#collapseTwo" href="#">
141 <a data-toggle="collapse" data-target="#collapseTwo" href="#">
142 Notebooks
142 Notebooks
143 </a>
143 </a>
144 </div>
144 </div>
145 <div id="collapseTwo" class=" collapse in">
145 <div id="collapseTwo" class=" collapse in">
146 <div class="panel-body">
146 <div class="panel-body">
147 <div id="running_list">
147 <div id="running_list">
148 <div id="running_list_placeholder" class="row list_placeholder">
148 <div id="running_list_placeholder" class="row list_placeholder">
149 <div> There are no notebooks running. </div>
149 <div> There are no notebooks running. </div>
150 </div>
150 </div>
151 </div>
151 </div>
152 </div>
152 </div>
153 </div>
153 </div>
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157 <div id="clusters" class="tab-pane">
157 <div id="clusters" class="tab-pane">
158 <div id="cluster_toolbar" class="row">
158 <div id="cluster_toolbar" class="row">
159 <div class="col-xs-8 no-padding">
159 <div class="col-xs-8 no-padding">
160 <span id="cluster_list_info">IPython parallel computing clusters</span>
160 <span id="cluster_list_info">IPython parallel computing clusters</span>
161 </div>
161 </div>
162 <div class="col-xs-4 no-padding tree-buttons">
162 <div class="col-xs-4 no-padding tree-buttons">
163 <span id="cluster_buttons" class="pull-right">
163 <span id="cluster_buttons" class="pull-right">
164 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
164 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
165 </span>
165 </span>
166 </div>
166 </div>
167 </div>
167 </div>
168 <div id="cluster_list">
168 <div id="cluster_list">
169 <div id="cluster_list_header" class="row list_header">
169 <div id="cluster_list_header" class="row list_header">
170 <div class="profile_col col-xs-4">profile</div>
170 <div class="profile_col col-xs-4">profile</div>
171 <div class="status_col col-xs-3">status</div>
171 <div class="status_col col-xs-3">status</div>
172 <div class="engines_col col-xs-3" title="Enter the number of engines to start or empty for default"># of engines</div>
172 <div class="engines_col col-xs-3" title="Enter the number of engines to start or empty for default"># of engines</div>
173 <div class="action_col col-xs-2">action</div>
173 <div class="action_col col-xs-2">action</div>
174 </div>
174 </div>
175 </div>
175 </div>
176 </div>
176 </div>
177 </div><!-- class:tab-content -->
177 </div><!-- class:tab-content -->
178 </div><!-- id:tab_content -->
178 </div><!-- id:tab_content -->
179 </div><!-- ipython-main-app -->
179 </div><!-- ipython-main-app -->
180
180
181 {% endblock %}
181 {% endblock %}
182
182
183 {% block script %}
183 {% block script %}
184 {{super()}}
184 {{super()}}
185
185
186 <script src="{{ static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
186 <script src="{{ static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
187 {% endblock %}
187 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now