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