##// END OF EJS Templates
remove unused project_dir
MinRK -
Show More
@@ -1,460 +1,456 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 try:
22 try:
23 from tornado.log import app_log
23 from tornado.log import app_log
24 except ImportError:
24 except ImportError:
25 app_log = logging.getLogger()
25 app_log = logging.getLogger()
26
26
27 from IPython.config import Application
27 from IPython.config import Application
28 from IPython.utils.path import filefind
28 from IPython.utils.path import filefind
29 from IPython.utils.py3compat import string_types
29 from IPython.utils.py3compat import string_types
30 from IPython.html.utils import is_hidden, url_path_join, url_escape
30 from IPython.html.utils import is_hidden, url_path_join, url_escape
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Top-level handlers
33 # Top-level handlers
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36
36
37 class AuthenticatedHandler(web.RequestHandler):
37 class AuthenticatedHandler(web.RequestHandler):
38 """A RequestHandler with an authenticated user."""
38 """A RequestHandler with an authenticated user."""
39
39
40 def set_default_headers(self):
40 def set_default_headers(self):
41 headers = self.settings.get('headers', {})
41 headers = self.settings.get('headers', {})
42
42
43 if "X-Frame-Options" not in headers:
43 if "X-Frame-Options" not in headers:
44 headers["X-Frame-Options"] = "SAMEORIGIN"
44 headers["X-Frame-Options"] = "SAMEORIGIN"
45
45
46 for header_name,value in headers.items() :
46 for header_name,value in headers.items() :
47 try:
47 try:
48 self.set_header(header_name, value)
48 self.set_header(header_name, value)
49 except Exception:
49 except Exception:
50 # tornado raise Exception (not a subclass)
50 # tornado raise Exception (not a subclass)
51 # if method is unsupported (websocket and Access-Control-Allow-Origin
51 # if method is unsupported (websocket and Access-Control-Allow-Origin
52 # for example, so just ignore)
52 # for example, so just ignore)
53 pass
53 pass
54
54
55 def clear_login_cookie(self):
55 def clear_login_cookie(self):
56 self.clear_cookie(self.cookie_name)
56 self.clear_cookie(self.cookie_name)
57
57
58 def get_current_user(self):
58 def get_current_user(self):
59 user_id = self.get_secure_cookie(self.cookie_name)
59 user_id = self.get_secure_cookie(self.cookie_name)
60 # For now the user_id should not return empty, but it could eventually
60 # For now the user_id should not return empty, but it could eventually
61 if user_id == '':
61 if user_id == '':
62 user_id = 'anonymous'
62 user_id = 'anonymous'
63 if user_id is None:
63 if user_id is None:
64 # prevent extra Invalid cookie sig warnings:
64 # prevent extra Invalid cookie sig warnings:
65 self.clear_login_cookie()
65 self.clear_login_cookie()
66 if not self.login_available:
66 if not self.login_available:
67 user_id = 'anonymous'
67 user_id = 'anonymous'
68 return user_id
68 return user_id
69
69
70 @property
70 @property
71 def cookie_name(self):
71 def cookie_name(self):
72 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
72 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
73 self.request.host
73 self.request.host
74 ))
74 ))
75 return self.settings.get('cookie_name', default_cookie_name)
75 return self.settings.get('cookie_name', default_cookie_name)
76
76
77 @property
77 @property
78 def password(self):
78 def password(self):
79 """our password"""
79 """our password"""
80 return self.settings.get('password', '')
80 return self.settings.get('password', '')
81
81
82 @property
82 @property
83 def logged_in(self):
83 def logged_in(self):
84 """Is a user currently logged in?
84 """Is a user currently logged in?
85
85
86 """
86 """
87 user = self.get_current_user()
87 user = self.get_current_user()
88 return (user and not user == 'anonymous')
88 return (user and not user == 'anonymous')
89
89
90 @property
90 @property
91 def login_available(self):
91 def login_available(self):
92 """May a user proceed to log in?
92 """May a user proceed to log in?
93
93
94 This returns True if login capability is available, irrespective of
94 This returns True if login capability is available, irrespective of
95 whether the user is already logged in or not.
95 whether the user is already logged in or not.
96
96
97 """
97 """
98 return bool(self.settings.get('password', ''))
98 return bool(self.settings.get('password', ''))
99
99
100
100
101 class IPythonHandler(AuthenticatedHandler):
101 class IPythonHandler(AuthenticatedHandler):
102 """IPython-specific extensions to authenticated handling
102 """IPython-specific extensions to authenticated handling
103
103
104 Mostly property shortcuts to IPython-specific settings.
104 Mostly property shortcuts to IPython-specific settings.
105 """
105 """
106
106
107 @property
107 @property
108 def config(self):
108 def config(self):
109 return self.settings.get('config', None)
109 return self.settings.get('config', None)
110
110
111 @property
111 @property
112 def log(self):
112 def log(self):
113 """use the IPython log by default, falling back on tornado's logger"""
113 """use the IPython log by default, falling back on tornado's logger"""
114 if Application.initialized():
114 if Application.initialized():
115 return Application.instance().log
115 return Application.instance().log
116 else:
116 else:
117 return app_log
117 return app_log
118
118
119 #---------------------------------------------------------------
119 #---------------------------------------------------------------
120 # URLs
120 # URLs
121 #---------------------------------------------------------------
121 #---------------------------------------------------------------
122
122
123 @property
123 @property
124 def mathjax_url(self):
124 def mathjax_url(self):
125 return self.settings.get('mathjax_url', '')
125 return self.settings.get('mathjax_url', '')
126
126
127 @property
127 @property
128 def base_url(self):
128 def base_url(self):
129 return self.settings.get('base_url', '/')
129 return self.settings.get('base_url', '/')
130
130
131 @property
131 @property
132 def ws_url(self):
132 def ws_url(self):
133 return self.settings.get('websocket_url', '')
133 return self.settings.get('websocket_url', '')
134
134
135 #---------------------------------------------------------------
135 #---------------------------------------------------------------
136 # Manager objects
136 # Manager objects
137 #---------------------------------------------------------------
137 #---------------------------------------------------------------
138
138
139 @property
139 @property
140 def kernel_manager(self):
140 def kernel_manager(self):
141 return self.settings['kernel_manager']
141 return self.settings['kernel_manager']
142
142
143 @property
143 @property
144 def contents_manager(self):
144 def contents_manager(self):
145 return self.settings['contents_manager']
145 return self.settings['contents_manager']
146
146
147 @property
147 @property
148 def cluster_manager(self):
148 def cluster_manager(self):
149 return self.settings['cluster_manager']
149 return self.settings['cluster_manager']
150
150
151 @property
151 @property
152 def session_manager(self):
152 def session_manager(self):
153 return self.settings['session_manager']
153 return self.settings['session_manager']
154
154
155 @property
155 @property
156 def kernel_spec_manager(self):
156 def kernel_spec_manager(self):
157 return self.settings['kernel_spec_manager']
157 return self.settings['kernel_spec_manager']
158
158
159 @property
160 def project_dir(self):
161 return getattr(self.contents_manager, 'root_dir', '/')
162
163 #---------------------------------------------------------------
159 #---------------------------------------------------------------
164 # CORS
160 # CORS
165 #---------------------------------------------------------------
161 #---------------------------------------------------------------
166
162
167 @property
163 @property
168 def allow_origin(self):
164 def allow_origin(self):
169 """Normal Access-Control-Allow-Origin"""
165 """Normal Access-Control-Allow-Origin"""
170 return self.settings.get('allow_origin', '')
166 return self.settings.get('allow_origin', '')
171
167
172 @property
168 @property
173 def allow_origin_pat(self):
169 def allow_origin_pat(self):
174 """Regular expression version of allow_origin"""
170 """Regular expression version of allow_origin"""
175 return self.settings.get('allow_origin_pat', None)
171 return self.settings.get('allow_origin_pat', None)
176
172
177 @property
173 @property
178 def allow_credentials(self):
174 def allow_credentials(self):
179 """Whether to set Access-Control-Allow-Credentials"""
175 """Whether to set Access-Control-Allow-Credentials"""
180 return self.settings.get('allow_credentials', False)
176 return self.settings.get('allow_credentials', False)
181
177
182 def set_default_headers(self):
178 def set_default_headers(self):
183 """Add CORS headers, if defined"""
179 """Add CORS headers, if defined"""
184 super(IPythonHandler, self).set_default_headers()
180 super(IPythonHandler, self).set_default_headers()
185 if self.allow_origin:
181 if self.allow_origin:
186 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
182 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
187 elif self.allow_origin_pat:
183 elif self.allow_origin_pat:
188 origin = self.get_origin()
184 origin = self.get_origin()
189 if origin and self.allow_origin_pat.match(origin):
185 if origin and self.allow_origin_pat.match(origin):
190 self.set_header("Access-Control-Allow-Origin", origin)
186 self.set_header("Access-Control-Allow-Origin", origin)
191 if self.allow_credentials:
187 if self.allow_credentials:
192 self.set_header("Access-Control-Allow-Credentials", 'true')
188 self.set_header("Access-Control-Allow-Credentials", 'true')
193
189
194 def get_origin(self):
190 def get_origin(self):
195 # Handle WebSocket Origin naming convention differences
191 # Handle WebSocket Origin naming convention differences
196 # The difference between version 8 and 13 is that in 8 the
192 # The difference between version 8 and 13 is that in 8 the
197 # client sends a "Sec-Websocket-Origin" header and in 13 it's
193 # client sends a "Sec-Websocket-Origin" header and in 13 it's
198 # simply "Origin".
194 # simply "Origin".
199 if "Origin" in self.request.headers:
195 if "Origin" in self.request.headers:
200 origin = self.request.headers.get("Origin")
196 origin = self.request.headers.get("Origin")
201 else:
197 else:
202 origin = self.request.headers.get("Sec-Websocket-Origin", None)
198 origin = self.request.headers.get("Sec-Websocket-Origin", None)
203 return origin
199 return origin
204
200
205 #---------------------------------------------------------------
201 #---------------------------------------------------------------
206 # template rendering
202 # template rendering
207 #---------------------------------------------------------------
203 #---------------------------------------------------------------
208
204
209 def get_template(self, name):
205 def get_template(self, name):
210 """Return the jinja template object for a given name"""
206 """Return the jinja template object for a given name"""
211 return self.settings['jinja2_env'].get_template(name)
207 return self.settings['jinja2_env'].get_template(name)
212
208
213 def render_template(self, name, **ns):
209 def render_template(self, name, **ns):
214 ns.update(self.template_namespace)
210 ns.update(self.template_namespace)
215 template = self.get_template(name)
211 template = self.get_template(name)
216 return template.render(**ns)
212 return template.render(**ns)
217
213
218 @property
214 @property
219 def template_namespace(self):
215 def template_namespace(self):
220 return dict(
216 return dict(
221 base_url=self.base_url,
217 base_url=self.base_url,
222 ws_url=self.ws_url,
218 ws_url=self.ws_url,
223 logged_in=self.logged_in,
219 logged_in=self.logged_in,
224 login_available=self.login_available,
220 login_available=self.login_available,
225 static_url=self.static_url,
221 static_url=self.static_url,
226 )
222 )
227
223
228 def get_json_body(self):
224 def get_json_body(self):
229 """Return the body of the request as JSON data."""
225 """Return the body of the request as JSON data."""
230 if not self.request.body:
226 if not self.request.body:
231 return None
227 return None
232 # Do we need to call body.decode('utf-8') here?
228 # Do we need to call body.decode('utf-8') here?
233 body = self.request.body.strip().decode(u'utf-8')
229 body = self.request.body.strip().decode(u'utf-8')
234 try:
230 try:
235 model = json.loads(body)
231 model = json.loads(body)
236 except Exception:
232 except Exception:
237 self.log.debug("Bad JSON: %r", body)
233 self.log.debug("Bad JSON: %r", body)
238 self.log.error("Couldn't parse JSON", exc_info=True)
234 self.log.error("Couldn't parse JSON", exc_info=True)
239 raise web.HTTPError(400, u'Invalid JSON in body of request')
235 raise web.HTTPError(400, u'Invalid JSON in body of request')
240 return model
236 return model
241
237
242 def get_error_html(self, status_code, **kwargs):
238 def get_error_html(self, status_code, **kwargs):
243 """render custom error pages"""
239 """render custom error pages"""
244 exception = kwargs.get('exception')
240 exception = kwargs.get('exception')
245 message = ''
241 message = ''
246 status_message = responses.get(status_code, 'Unknown HTTP Error')
242 status_message = responses.get(status_code, 'Unknown HTTP Error')
247 if exception:
243 if exception:
248 # get the custom message, if defined
244 # get the custom message, if defined
249 try:
245 try:
250 message = exception.log_message % exception.args
246 message = exception.log_message % exception.args
251 except Exception:
247 except Exception:
252 pass
248 pass
253
249
254 # construct the custom reason, if defined
250 # construct the custom reason, if defined
255 reason = getattr(exception, 'reason', '')
251 reason = getattr(exception, 'reason', '')
256 if reason:
252 if reason:
257 status_message = reason
253 status_message = reason
258
254
259 # build template namespace
255 # build template namespace
260 ns = dict(
256 ns = dict(
261 status_code=status_code,
257 status_code=status_code,
262 status_message=status_message,
258 status_message=status_message,
263 message=message,
259 message=message,
264 exception=exception,
260 exception=exception,
265 )
261 )
266
262
267 # render the template
263 # render the template
268 try:
264 try:
269 html = self.render_template('%s.html' % status_code, **ns)
265 html = self.render_template('%s.html' % status_code, **ns)
270 except TemplateNotFound:
266 except TemplateNotFound:
271 self.log.debug("No template for %d", status_code)
267 self.log.debug("No template for %d", status_code)
272 html = self.render_template('error.html', **ns)
268 html = self.render_template('error.html', **ns)
273 return html
269 return html
274
270
275
271
276 class Template404(IPythonHandler):
272 class Template404(IPythonHandler):
277 """Render our 404 template"""
273 """Render our 404 template"""
278 def prepare(self):
274 def prepare(self):
279 raise web.HTTPError(404)
275 raise web.HTTPError(404)
280
276
281
277
282 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
278 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
283 """static files should only be accessible when logged in"""
279 """static files should only be accessible when logged in"""
284
280
285 @web.authenticated
281 @web.authenticated
286 def get(self, path):
282 def get(self, path):
287 if os.path.splitext(path)[1] == '.ipynb':
283 if os.path.splitext(path)[1] == '.ipynb':
288 name = os.path.basename(path)
284 name = os.path.basename(path)
289 self.set_header('Content-Type', 'application/json')
285 self.set_header('Content-Type', 'application/json')
290 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
286 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
291
287
292 return web.StaticFileHandler.get(self, path)
288 return web.StaticFileHandler.get(self, path)
293
289
294 def compute_etag(self):
290 def compute_etag(self):
295 return None
291 return None
296
292
297 def validate_absolute_path(self, root, absolute_path):
293 def validate_absolute_path(self, root, absolute_path):
298 """Validate and return the absolute path.
294 """Validate and return the absolute path.
299
295
300 Requires tornado 3.1
296 Requires tornado 3.1
301
297
302 Adding to tornado's own handling, forbids the serving of hidden files.
298 Adding to tornado's own handling, forbids the serving of hidden files.
303 """
299 """
304 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
300 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
305 abs_root = os.path.abspath(root)
301 abs_root = os.path.abspath(root)
306 if is_hidden(abs_path, abs_root):
302 if is_hidden(abs_path, abs_root):
307 self.log.info("Refusing to serve hidden file, via 404 Error")
303 self.log.info("Refusing to serve hidden file, via 404 Error")
308 raise web.HTTPError(404)
304 raise web.HTTPError(404)
309 return abs_path
305 return abs_path
310
306
311
307
312 def json_errors(method):
308 def json_errors(method):
313 """Decorate methods with this to return GitHub style JSON errors.
309 """Decorate methods with this to return GitHub style JSON errors.
314
310
315 This should be used on any JSON API on any handler method that can raise HTTPErrors.
311 This should be used on any JSON API on any handler method that can raise HTTPErrors.
316
312
317 This will grab the latest HTTPError exception using sys.exc_info
313 This will grab the latest HTTPError exception using sys.exc_info
318 and then:
314 and then:
319
315
320 1. Set the HTTP status code based on the HTTPError
316 1. Set the HTTP status code based on the HTTPError
321 2. Create and return a JSON body with a message field describing
317 2. Create and return a JSON body with a message field describing
322 the error in a human readable form.
318 the error in a human readable form.
323 """
319 """
324 @functools.wraps(method)
320 @functools.wraps(method)
325 def wrapper(self, *args, **kwargs):
321 def wrapper(self, *args, **kwargs):
326 try:
322 try:
327 result = method(self, *args, **kwargs)
323 result = method(self, *args, **kwargs)
328 except web.HTTPError as e:
324 except web.HTTPError as e:
329 status = e.status_code
325 status = e.status_code
330 message = e.log_message
326 message = e.log_message
331 self.log.warn(message)
327 self.log.warn(message)
332 self.set_status(e.status_code)
328 self.set_status(e.status_code)
333 self.finish(json.dumps(dict(message=message)))
329 self.finish(json.dumps(dict(message=message)))
334 except Exception:
330 except Exception:
335 self.log.error("Unhandled error in API request", exc_info=True)
331 self.log.error("Unhandled error in API request", exc_info=True)
336 status = 500
332 status = 500
337 message = "Unknown server error"
333 message = "Unknown server error"
338 t, value, tb = sys.exc_info()
334 t, value, tb = sys.exc_info()
339 self.set_status(status)
335 self.set_status(status)
340 tb_text = ''.join(traceback.format_exception(t, value, tb))
336 tb_text = ''.join(traceback.format_exception(t, value, tb))
341 reply = dict(message=message, traceback=tb_text)
337 reply = dict(message=message, traceback=tb_text)
342 self.finish(json.dumps(reply))
338 self.finish(json.dumps(reply))
343 else:
339 else:
344 return result
340 return result
345 return wrapper
341 return wrapper
346
342
347
343
348
344
349 #-----------------------------------------------------------------------------
345 #-----------------------------------------------------------------------------
350 # File handler
346 # File handler
351 #-----------------------------------------------------------------------------
347 #-----------------------------------------------------------------------------
352
348
353 # to minimize subclass changes:
349 # to minimize subclass changes:
354 HTTPError = web.HTTPError
350 HTTPError = web.HTTPError
355
351
356 class FileFindHandler(web.StaticFileHandler):
352 class FileFindHandler(web.StaticFileHandler):
357 """subclass of StaticFileHandler for serving files from a search path"""
353 """subclass of StaticFileHandler for serving files from a search path"""
358
354
359 # cache search results, don't search for files more than once
355 # cache search results, don't search for files more than once
360 _static_paths = {}
356 _static_paths = {}
361
357
362 def initialize(self, path, default_filename=None):
358 def initialize(self, path, default_filename=None):
363 if isinstance(path, string_types):
359 if isinstance(path, string_types):
364 path = [path]
360 path = [path]
365
361
366 self.root = tuple(
362 self.root = tuple(
367 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
363 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
368 )
364 )
369 self.default_filename = default_filename
365 self.default_filename = default_filename
370
366
371 def compute_etag(self):
367 def compute_etag(self):
372 return None
368 return None
373
369
374 @classmethod
370 @classmethod
375 def get_absolute_path(cls, roots, path):
371 def get_absolute_path(cls, roots, path):
376 """locate a file to serve on our static file search path"""
372 """locate a file to serve on our static file search path"""
377 with cls._lock:
373 with cls._lock:
378 if path in cls._static_paths:
374 if path in cls._static_paths:
379 return cls._static_paths[path]
375 return cls._static_paths[path]
380 try:
376 try:
381 abspath = os.path.abspath(filefind(path, roots))
377 abspath = os.path.abspath(filefind(path, roots))
382 except IOError:
378 except IOError:
383 # IOError means not found
379 # IOError means not found
384 return ''
380 return ''
385
381
386 cls._static_paths[path] = abspath
382 cls._static_paths[path] = abspath
387 return abspath
383 return abspath
388
384
389 def validate_absolute_path(self, root, absolute_path):
385 def validate_absolute_path(self, root, absolute_path):
390 """check if the file should be served (raises 404, 403, etc.)"""
386 """check if the file should be served (raises 404, 403, etc.)"""
391 if absolute_path == '':
387 if absolute_path == '':
392 raise web.HTTPError(404)
388 raise web.HTTPError(404)
393
389
394 for root in self.root:
390 for root in self.root:
395 if (absolute_path + os.sep).startswith(root):
391 if (absolute_path + os.sep).startswith(root):
396 break
392 break
397
393
398 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
394 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
399
395
400
396
401 class TrailingSlashHandler(web.RequestHandler):
397 class TrailingSlashHandler(web.RequestHandler):
402 """Simple redirect handler that strips trailing slashes
398 """Simple redirect handler that strips trailing slashes
403
399
404 This should be the first, highest priority handler.
400 This should be the first, highest priority handler.
405 """
401 """
406
402
407 SUPPORTED_METHODS = ['GET']
403 SUPPORTED_METHODS = ['GET']
408
404
409 def get(self):
405 def get(self):
410 self.redirect(self.request.uri.rstrip('/'))
406 self.redirect(self.request.uri.rstrip('/'))
411
407
412
408
413 class FilesRedirectHandler(IPythonHandler):
409 class FilesRedirectHandler(IPythonHandler):
414 """Handler for redirecting relative URLs to the /files/ handler"""
410 """Handler for redirecting relative URLs to the /files/ handler"""
415 def get(self, path=''):
411 def get(self, path=''):
416 cm = self.contents_manager
412 cm = self.contents_manager
417 if cm.path_exists(path):
413 if cm.path_exists(path):
418 # it's a *directory*, redirect to /tree
414 # it's a *directory*, redirect to /tree
419 url = url_path_join(self.base_url, 'tree', path)
415 url = url_path_join(self.base_url, 'tree', path)
420 else:
416 else:
421 orig_path = path
417 orig_path = path
422 # otherwise, redirect to /files
418 # otherwise, redirect to /files
423 parts = path.split('/')
419 parts = path.split('/')
424 path = '/'.join(parts[:-1])
420 path = '/'.join(parts[:-1])
425 name = parts[-1]
421 name = parts[-1]
426
422
427 if not cm.file_exists(name=name, path=path) and 'files' in parts:
423 if not cm.file_exists(name=name, path=path) and 'files' in parts:
428 # redirect without files/ iff it would 404
424 # redirect without files/ iff it would 404
429 # this preserves pre-2.0-style 'files/' links
425 # this preserves pre-2.0-style 'files/' links
430 self.log.warn("Deprecated files/ URL: %s", orig_path)
426 self.log.warn("Deprecated files/ URL: %s", orig_path)
431 parts.remove('files')
427 parts.remove('files')
432 path = '/'.join(parts[:-1])
428 path = '/'.join(parts[:-1])
433
429
434 if not cm.file_exists(name=name, path=path):
430 if not cm.file_exists(name=name, path=path):
435 raise web.HTTPError(404)
431 raise web.HTTPError(404)
436
432
437 url = url_path_join(self.base_url, 'files', path, name)
433 url = url_path_join(self.base_url, 'files', path, name)
438 url = url_escape(url)
434 url = url_escape(url)
439 self.log.debug("Redirecting %s to %s", self.request.path, url)
435 self.log.debug("Redirecting %s to %s", self.request.path, url)
440 self.redirect(url)
436 self.redirect(url)
441
437
442
438
443 #-----------------------------------------------------------------------------
439 #-----------------------------------------------------------------------------
444 # URL pattern fragments for re-use
440 # URL pattern fragments for re-use
445 #-----------------------------------------------------------------------------
441 #-----------------------------------------------------------------------------
446
442
447 path_regex = r"(?P<path>(?:/.*)*)"
443 path_regex = r"(?P<path>(?:/.*)*)"
448 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
444 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
449 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
445 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
450 file_name_regex = r"(?P<name>[^/]+)"
446 file_name_regex = r"(?P<name>[^/]+)"
451 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
447 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
452
448
453 #-----------------------------------------------------------------------------
449 #-----------------------------------------------------------------------------
454 # URL to handler mappings
450 # URL to handler mappings
455 #-----------------------------------------------------------------------------
451 #-----------------------------------------------------------------------------
456
452
457
453
458 default_handlers = [
454 default_handlers = [
459 (r".*/", TrailingSlashHandler)
455 (r".*/", TrailingSlashHandler)
460 ]
456 ]
@@ -1,52 +1,51 b''
1 """Tornado handlers for the live notebook view."""
1 """Tornado handlers for the live notebook view."""
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 os
6 import os
7 from tornado import web
7 from tornado import web
8 HTTPError = web.HTTPError
8 HTTPError = web.HTTPError
9
9
10 from ..base.handlers import (
10 from ..base.handlers import (
11 IPythonHandler, FilesRedirectHandler,
11 IPythonHandler, FilesRedirectHandler,
12 notebook_path_regex, path_regex,
12 notebook_path_regex, path_regex,
13 )
13 )
14 from ..utils import url_escape
14 from ..utils import url_escape
15
15
16
16
17 class NotebookHandler(IPythonHandler):
17 class NotebookHandler(IPythonHandler):
18
18
19 @web.authenticated
19 @web.authenticated
20 def get(self, path='', name=None):
20 def get(self, path='', name=None):
21 """get renders the notebook template if a name is given, or
21 """get renders the notebook template if a name is given, or
22 redirects to the '/files/' handler if the name is not given."""
22 redirects to the '/files/' handler if the name is not given."""
23 path = path.strip('/')
23 path = path.strip('/')
24 cm = self.contents_manager
24 cm = self.contents_manager
25 if name is None:
25 if name is None:
26 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
26 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
27
27
28 # a .ipynb filename was given
28 # a .ipynb filename was given
29 if not cm.file_exists(name, path):
29 if not cm.file_exists(name, path):
30 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
30 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
31 name = url_escape(name)
31 name = url_escape(name)
32 path = url_escape(path)
32 path = url_escape(path)
33 self.write(self.render_template('notebook.html',
33 self.write(self.render_template('notebook.html',
34 project=self.project_dir,
35 notebook_path=path,
34 notebook_path=path,
36 notebook_name=name,
35 notebook_name=name,
37 kill_kernel=False,
36 kill_kernel=False,
38 mathjax_url=self.mathjax_url,
37 mathjax_url=self.mathjax_url,
39 )
38 )
40 )
39 )
41
40
42
41
43 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
44 # URL to handler mappings
43 # URL to handler mappings
45 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
46
45
47
46
48 default_handlers = [
47 default_handlers = [
49 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
48 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
50 (r"/notebooks%s" % path_regex, FilesRedirectHandler),
49 (r"/notebooks%s" % path_regex, FilesRedirectHandler),
51 ]
50 ]
52
51
@@ -1,116 +1,115 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 stylesheet %}
6 {% block stylesheet %}
7 {{super()}}
7 {{super()}}
8 <link rel="stylesheet" href="{{ static_url("tree/css/override.css") }}" type="text/css" />
8 <link rel="stylesheet" href="{{ static_url("tree/css/override.css") }}" type="text/css" />
9 {% endblock %}
9 {% endblock %}
10
10
11 {% block params %}
11 {% block params %}
12
12
13 data-project="{{project}}"
14 data-base-url="{{base_url}}"
13 data-base-url="{{base_url}}"
15 data-notebook-path="{{notebook_path}}"
14 data-notebook-path="{{notebook_path}}"
16
15
17 {% endblock %}
16 {% endblock %}
18
17
19
18
20 {% block site %}
19 {% block site %}
21
20
22 <div id="ipython-main-app" class="container">
21 <div id="ipython-main-app" class="container">
23
22
24 <div id="tab_content" class="tabbable">
23 <div id="tab_content" class="tabbable">
25 <ul id="tabs" class="nav nav-tabs">
24 <ul id="tabs" class="nav nav-tabs">
26 <li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
25 <li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
27 <li><a href="#running" data-toggle="tab">Running</a></li>
26 <li><a href="#running" data-toggle="tab">Running</a></li>
28 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
27 <li><a href="#clusters" data-toggle="tab">Clusters</a></li>
29 </ul>
28 </ul>
30
29
31 <div class="tab-content">
30 <div class="tab-content">
32 <div id="notebooks" class="tab-pane active">
31 <div id="notebooks" class="tab-pane active">
33 <div id="notebook_toolbar" class="row">
32 <div id="notebook_toolbar" class="row">
34 <div class="col-sm-8 no-padding">
33 <div class="col-sm-8 no-padding">
35 <form id='alternate_upload' class='alternate_upload' >
34 <form id='alternate_upload' class='alternate_upload' >
36 <span id="notebook_list_info" style="position:absolute" >
35 <span id="notebook_list_info" style="position:absolute" >
37 To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
36 To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
38 </span>
37 </span>
39 <input type="file" name="datafile" class="fileinput" multiple='multiple'>
38 <input type="file" name="datafile" class="fileinput" multiple='multiple'>
40 </form>
39 </form>
41 </div>
40 </div>
42 <div class="col-sm-4 no-padding tree-buttons">
41 <div class="col-sm-4 no-padding tree-buttons">
43 <span id="notebook_buttons" class="pull-right">
42 <span id="notebook_buttons" class="pull-right">
44 <button id="new_notebook" title="Create new notebook" class="btn btn-default btn-xs">New Notebook</button>
43 <button id="new_notebook" title="Create new notebook" class="btn btn-default btn-xs">New Notebook</button>
45 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
44 <button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
46 </span>
45 </span>
47 </div>
46 </div>
48 </div>
47 </div>
49
48
50 <div id="notebook_list">
49 <div id="notebook_list">
51 <div id="notebook_list_header" class="row list_header">
50 <div id="notebook_list_header" class="row list_header">
52 <div id="project_name">
51 <div id="project_name">
53 <ul class="breadcrumb">
52 <ul class="breadcrumb">
54 <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-home"></i></a></li>
53 <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-home"></i></a></li>
55 {% for crumb in breadcrumbs[1:] %}
54 {% for crumb in breadcrumbs[1:] %}
56 <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
55 <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
57 {% endfor %}
56 {% endfor %}
58 </ul>
57 </ul>
59 </div>
58 </div>
60 </div>
59 </div>
61 </div>
60 </div>
62 </div>
61 </div>
63
62
64 <div id="running" class="tab-pane">
63 <div id="running" class="tab-pane">
65
64
66 <div id="running_toolbar" class="row">
65 <div id="running_toolbar" class="row">
67 <div class="col-sm-8 no-padding">
66 <div class="col-sm-8 no-padding">
68 <span id="running_list_info">Currently running IPython notebooks</span>
67 <span id="running_list_info">Currently running IPython notebooks</span>
69 </div>
68 </div>
70 <div class="col-sm-4 no-padding tree-buttons">
69 <div class="col-sm-4 no-padding tree-buttons">
71 <span id="running_buttons" class="pull-right">
70 <span id="running_buttons" class="pull-right">
72 <button id="refresh_running_list" title="Refresh running list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
71 <button id="refresh_running_list" title="Refresh running list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
73 </span>
72 </span>
74 </div>
73 </div>
75 </div>
74 </div>
76
75
77 <div id="running_list">
76 <div id="running_list">
78 <div id="running_list_header" class="row list_header">
77 <div id="running_list_header" class="row list_header">
79 <div> There are no notebooks running. </div>
78 <div> There are no notebooks running. </div>
80 </div>
79 </div>
81 </div>
80 </div>
82 </div>
81 </div>
83
82
84 <div id="clusters" class="tab-pane">
83 <div id="clusters" class="tab-pane">
85
84
86 <div id="cluster_toolbar" class="row">
85 <div id="cluster_toolbar" class="row">
87 <div class="col-xs-8 no-padding">
86 <div class="col-xs-8 no-padding">
88 <span id="cluster_list_info">IPython parallel computing clusters</span>
87 <span id="cluster_list_info">IPython parallel computing clusters</span>
89 </div>
88 </div>
90 <div class="col-xs-4 no-padding tree-buttons">
89 <div class="col-xs-4 no-padding tree-buttons">
91 <span id="cluster_buttons" class="pull-right">
90 <span id="cluster_buttons" class="pull-right">
92 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
91 <button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i></button>
93 </span>
92 </span>
94 </div>
93 </div>
95 </div>
94 </div>
96
95
97 <div id="cluster_list">
96 <div id="cluster_list">
98 <div id="cluster_list_header" class="row list_header">
97 <div id="cluster_list_header" class="row list_header">
99 <div class="profile_col col-xs-4">profile</div>
98 <div class="profile_col col-xs-4">profile</div>
100 <div class="status_col col-xs-3">status</div>
99 <div class="status_col col-xs-3">status</div>
101 <div class="engines_col col-xs-3" title="Enter the number of engines to start or empty for default"># of engines</div>
100 <div class="engines_col col-xs-3" title="Enter the number of engines to start or empty for default"># of engines</div>
102 <div class="action_col col-xs-2">action</div>
101 <div class="action_col col-xs-2">action</div>
103 </div>
102 </div>
104 </div>
103 </div>
105 </div>
104 </div>
106 </div>
105 </div>
107
106
108 </div>
107 </div>
109
108
110 {% endblock %}
109 {% endblock %}
111
110
112 {% block script %}
111 {% block script %}
113 {{super()}}
112 {{super()}}
114
113
115 <script src="{{ static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
114 <script src="{{ static_url("tree/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
116 {% endblock %}
115 {% endblock %}
@@ -1,85 +1,84 b''
1 """Tornado handlers for the tree view."""
1 """Tornado handlers for the tree view."""
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 from tornado import web
6 from tornado import web
7 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
7 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
8 from ..utils import url_path_join, url_escape
8 from ..utils import url_path_join, url_escape
9
9
10
10
11 class TreeHandler(IPythonHandler):
11 class TreeHandler(IPythonHandler):
12 """Render the tree view, listing notebooks, clusters, etc."""
12 """Render the tree view, listing notebooks, clusters, etc."""
13
13
14 def generate_breadcrumbs(self, path):
14 def generate_breadcrumbs(self, path):
15 breadcrumbs = [(url_escape(url_path_join(self.base_url, 'tree')), '')]
15 breadcrumbs = [(url_escape(url_path_join(self.base_url, 'tree')), '')]
16 comps = path.split('/')
16 comps = path.split('/')
17 ncomps = len(comps)
17 ncomps = len(comps)
18 for i in range(ncomps):
18 for i in range(ncomps):
19 if comps[i]:
19 if comps[i]:
20 link = url_escape(url_path_join(self.base_url, 'tree', *comps[0:i+1]))
20 link = url_escape(url_path_join(self.base_url, 'tree', *comps[0:i+1]))
21 breadcrumbs.append((link, comps[i]))
21 breadcrumbs.append((link, comps[i]))
22 return breadcrumbs
22 return breadcrumbs
23
23
24 def generate_page_title(self, path):
24 def generate_page_title(self, path):
25 comps = path.split('/')
25 comps = path.split('/')
26 if len(comps) > 3:
26 if len(comps) > 3:
27 for i in range(len(comps)-2):
27 for i in range(len(comps)-2):
28 comps.pop(0)
28 comps.pop(0)
29 page_title = url_path_join(*comps)
29 page_title = url_path_join(*comps)
30 if page_title:
30 if page_title:
31 return page_title+'/'
31 return page_title+'/'
32 else:
32 else:
33 return 'Home'
33 return 'Home'
34
34
35 @web.authenticated
35 @web.authenticated
36 def get(self, path='', name=None):
36 def get(self, path='', name=None):
37 path = path.strip('/')
37 path = path.strip('/')
38 cm = self.contents_manager
38 cm = self.contents_manager
39 if name is not None:
39 if name is not None:
40 # is a notebook, redirect to notebook handler
40 # is a notebook, redirect to notebook handler
41 url = url_escape(url_path_join(
41 url = url_escape(url_path_join(
42 self.base_url, 'notebooks', path, name
42 self.base_url, 'notebooks', path, name
43 ))
43 ))
44 self.log.debug("Redirecting %s to %s", self.request.path, url)
44 self.log.debug("Redirecting %s to %s", self.request.path, url)
45 self.redirect(url)
45 self.redirect(url)
46 else:
46 else:
47 if not cm.path_exists(path=path):
47 if not cm.path_exists(path=path):
48 # Directory is hidden or does not exist.
48 # Directory is hidden or does not exist.
49 raise web.HTTPError(404)
49 raise web.HTTPError(404)
50 elif cm.is_hidden(path):
50 elif cm.is_hidden(path):
51 self.log.info("Refusing to serve hidden directory, via 404 Error")
51 self.log.info("Refusing to serve hidden directory, via 404 Error")
52 raise web.HTTPError(404)
52 raise web.HTTPError(404)
53 breadcrumbs = self.generate_breadcrumbs(path)
53 breadcrumbs = self.generate_breadcrumbs(path)
54 page_title = self.generate_page_title(path)
54 page_title = self.generate_page_title(path)
55 self.write(self.render_template('tree.html',
55 self.write(self.render_template('tree.html',
56 project=self.project_dir,
57 page_title=page_title,
56 page_title=page_title,
58 notebook_path=path,
57 notebook_path=path,
59 breadcrumbs=breadcrumbs
58 breadcrumbs=breadcrumbs
60 ))
59 ))
61
60
62
61
63 class TreeRedirectHandler(IPythonHandler):
62 class TreeRedirectHandler(IPythonHandler):
64 """Redirect a request to the corresponding tree URL"""
63 """Redirect a request to the corresponding tree URL"""
65
64
66 @web.authenticated
65 @web.authenticated
67 def get(self, path=''):
66 def get(self, path=''):
68 url = url_escape(url_path_join(
67 url = url_escape(url_path_join(
69 self.base_url, 'tree', path.strip('/')
68 self.base_url, 'tree', path.strip('/')
70 ))
69 ))
71 self.log.debug("Redirecting %s to %s", self.request.path, url)
70 self.log.debug("Redirecting %s to %s", self.request.path, url)
72 self.redirect(url)
71 self.redirect(url)
73
72
74
73
75 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
76 # URL to handler mappings
75 # URL to handler mappings
77 #-----------------------------------------------------------------------------
76 #-----------------------------------------------------------------------------
78
77
79
78
80 default_handlers = [
79 default_handlers = [
81 (r"/tree%s" % notebook_path_regex, TreeHandler),
80 (r"/tree%s" % notebook_path_regex, TreeHandler),
82 (r"/tree%s" % path_regex, TreeHandler),
81 (r"/tree%s" % path_regex, TreeHandler),
83 (r"/tree", TreeHandler),
82 (r"/tree", TreeHandler),
84 (r"", TreeRedirectHandler),
83 (r"", TreeRedirectHandler),
85 ]
84 ]
General Comments 0
You need to be logged in to leave comments. Login now