##// END OF EJS Templates
Move notebook URL fragment regexen into IPython.html.base.handlers
Thomas Kluyver -
Show More
@@ -1,364 +1,372 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19
19
20 import functools
20 import functools
21 import json
21 import json
22 import logging
22 import logging
23 import os
23 import os
24 import stat
24 import stat
25 import sys
25 import sys
26 import traceback
26 import traceback
27
27
28 from tornado import web
28 from tornado import web
29
29
30 try:
30 try:
31 from tornado.log import app_log
31 from tornado.log import app_log
32 except ImportError:
32 except ImportError:
33 app_log = logging.getLogger()
33 app_log = logging.getLogger()
34
34
35 from IPython.config import Application
35 from IPython.config import Application
36 from IPython.utils.path import filefind
36 from IPython.utils.path import filefind
37 from IPython.utils.py3compat import string_types
37 from IPython.utils.py3compat import string_types
38
38
39 # UF_HIDDEN is a stat flag not defined in the stat module.
39 # UF_HIDDEN is a stat flag not defined in the stat module.
40 # It is used by BSD to indicate hidden files.
40 # It is used by BSD to indicate hidden files.
41 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
41 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Top-level handlers
44 # Top-level handlers
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 class RequestHandler(web.RequestHandler):
47 class RequestHandler(web.RequestHandler):
48 """RequestHandler with default variable setting."""
48 """RequestHandler with default variable setting."""
49
49
50 def render(*args, **kwargs):
50 def render(*args, **kwargs):
51 kwargs.setdefault('message', '')
51 kwargs.setdefault('message', '')
52 return web.RequestHandler.render(*args, **kwargs)
52 return web.RequestHandler.render(*args, **kwargs)
53
53
54 class AuthenticatedHandler(RequestHandler):
54 class AuthenticatedHandler(RequestHandler):
55 """A RequestHandler with an authenticated user."""
55 """A RequestHandler with an authenticated user."""
56
56
57 def clear_login_cookie(self):
57 def clear_login_cookie(self):
58 self.clear_cookie(self.cookie_name)
58 self.clear_cookie(self.cookie_name)
59
59
60 def get_current_user(self):
60 def get_current_user(self):
61 user_id = self.get_secure_cookie(self.cookie_name)
61 user_id = self.get_secure_cookie(self.cookie_name)
62 # For now the user_id should not return empty, but it could eventually
62 # For now the user_id should not return empty, but it could eventually
63 if user_id == '':
63 if user_id == '':
64 user_id = 'anonymous'
64 user_id = 'anonymous'
65 if user_id is None:
65 if user_id is None:
66 # prevent extra Invalid cookie sig warnings:
66 # prevent extra Invalid cookie sig warnings:
67 self.clear_login_cookie()
67 self.clear_login_cookie()
68 if not self.login_available:
68 if not self.login_available:
69 user_id = 'anonymous'
69 user_id = 'anonymous'
70 return user_id
70 return user_id
71
71
72 @property
72 @property
73 def cookie_name(self):
73 def cookie_name(self):
74 default_cookie_name = 'username-{host}'.format(
74 default_cookie_name = 'username-{host}'.format(
75 host=self.request.host,
75 host=self.request.host,
76 ).replace(':', '-')
76 ).replace(':', '-')
77 return self.settings.get('cookie_name', default_cookie_name)
77 return self.settings.get('cookie_name', default_cookie_name)
78
78
79 @property
79 @property
80 def password(self):
80 def password(self):
81 """our password"""
81 """our password"""
82 return self.settings.get('password', '')
82 return self.settings.get('password', '')
83
83
84 @property
84 @property
85 def logged_in(self):
85 def logged_in(self):
86 """Is a user currently logged in?
86 """Is a user currently logged in?
87
87
88 """
88 """
89 user = self.get_current_user()
89 user = self.get_current_user()
90 return (user and not user == 'anonymous')
90 return (user and not user == 'anonymous')
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 return bool(self.settings.get('password', ''))
100 return bool(self.settings.get('password', ''))
101
101
102
102
103 class IPythonHandler(AuthenticatedHandler):
103 class IPythonHandler(AuthenticatedHandler):
104 """IPython-specific extensions to authenticated handling
104 """IPython-specific extensions to authenticated handling
105
105
106 Mostly property shortcuts to IPython-specific settings.
106 Mostly property shortcuts to IPython-specific settings.
107 """
107 """
108
108
109 @property
109 @property
110 def config(self):
110 def config(self):
111 return self.settings.get('config', None)
111 return self.settings.get('config', None)
112
112
113 @property
113 @property
114 def log(self):
114 def log(self):
115 """use the IPython log by default, falling back on tornado's logger"""
115 """use the IPython log by default, falling back on tornado's logger"""
116 if Application.initialized():
116 if Application.initialized():
117 return Application.instance().log
117 return Application.instance().log
118 else:
118 else:
119 return app_log
119 return app_log
120
120
121 @property
121 @property
122 def use_less(self):
122 def use_less(self):
123 """Use less instead of css in templates"""
123 """Use less instead of css in templates"""
124 return self.settings.get('use_less', False)
124 return self.settings.get('use_less', False)
125
125
126 #---------------------------------------------------------------
126 #---------------------------------------------------------------
127 # URLs
127 # URLs
128 #---------------------------------------------------------------
128 #---------------------------------------------------------------
129
129
130 @property
130 @property
131 def ws_url(self):
131 def ws_url(self):
132 """websocket url matching the current request
132 """websocket url matching the current request
133
133
134 By default, this is just `''`, indicating that it should match
134 By default, this is just `''`, indicating that it should match
135 the same host, protocol, port, etc.
135 the same host, protocol, port, etc.
136 """
136 """
137 return self.settings.get('websocket_url', '')
137 return self.settings.get('websocket_url', '')
138
138
139 @property
139 @property
140 def mathjax_url(self):
140 def mathjax_url(self):
141 return self.settings.get('mathjax_url', '')
141 return self.settings.get('mathjax_url', '')
142
142
143 @property
143 @property
144 def base_project_url(self):
144 def base_project_url(self):
145 return self.settings.get('base_project_url', '/')
145 return self.settings.get('base_project_url', '/')
146
146
147 @property
147 @property
148 def base_kernel_url(self):
148 def base_kernel_url(self):
149 return self.settings.get('base_kernel_url', '/')
149 return self.settings.get('base_kernel_url', '/')
150
150
151 #---------------------------------------------------------------
151 #---------------------------------------------------------------
152 # Manager objects
152 # Manager objects
153 #---------------------------------------------------------------
153 #---------------------------------------------------------------
154
154
155 @property
155 @property
156 def kernel_manager(self):
156 def kernel_manager(self):
157 return self.settings['kernel_manager']
157 return self.settings['kernel_manager']
158
158
159 @property
159 @property
160 def notebook_manager(self):
160 def notebook_manager(self):
161 return self.settings['notebook_manager']
161 return self.settings['notebook_manager']
162
162
163 @property
163 @property
164 def cluster_manager(self):
164 def cluster_manager(self):
165 return self.settings['cluster_manager']
165 return self.settings['cluster_manager']
166
166
167 @property
167 @property
168 def session_manager(self):
168 def session_manager(self):
169 return self.settings['session_manager']
169 return self.settings['session_manager']
170
170
171 @property
171 @property
172 def project_dir(self):
172 def project_dir(self):
173 return self.notebook_manager.notebook_dir
173 return self.notebook_manager.notebook_dir
174
174
175 #---------------------------------------------------------------
175 #---------------------------------------------------------------
176 # template rendering
176 # template rendering
177 #---------------------------------------------------------------
177 #---------------------------------------------------------------
178
178
179 def get_template(self, name):
179 def get_template(self, name):
180 """Return the jinja template object for a given name"""
180 """Return the jinja template object for a given name"""
181 return self.settings['jinja2_env'].get_template(name)
181 return self.settings['jinja2_env'].get_template(name)
182
182
183 def render_template(self, name, **ns):
183 def render_template(self, name, **ns):
184 ns.update(self.template_namespace)
184 ns.update(self.template_namespace)
185 template = self.get_template(name)
185 template = self.get_template(name)
186 return template.render(**ns)
186 return template.render(**ns)
187
187
188 @property
188 @property
189 def template_namespace(self):
189 def template_namespace(self):
190 return dict(
190 return dict(
191 base_project_url=self.base_project_url,
191 base_project_url=self.base_project_url,
192 base_kernel_url=self.base_kernel_url,
192 base_kernel_url=self.base_kernel_url,
193 logged_in=self.logged_in,
193 logged_in=self.logged_in,
194 login_available=self.login_available,
194 login_available=self.login_available,
195 use_less=self.use_less,
195 use_less=self.use_less,
196 )
196 )
197
197
198 def get_json_body(self):
198 def get_json_body(self):
199 """Return the body of the request as JSON data."""
199 """Return the body of the request as JSON data."""
200 if not self.request.body:
200 if not self.request.body:
201 return None
201 return None
202 # Do we need to call body.decode('utf-8') here?
202 # Do we need to call body.decode('utf-8') here?
203 body = self.request.body.strip().decode(u'utf-8')
203 body = self.request.body.strip().decode(u'utf-8')
204 try:
204 try:
205 model = json.loads(body)
205 model = json.loads(body)
206 except Exception:
206 except Exception:
207 self.log.debug("Bad JSON: %r", body)
207 self.log.debug("Bad JSON: %r", body)
208 self.log.error("Couldn't parse JSON", exc_info=True)
208 self.log.error("Couldn't parse JSON", exc_info=True)
209 raise web.HTTPError(400, u'Invalid JSON in body of request')
209 raise web.HTTPError(400, u'Invalid JSON in body of request')
210 return model
210 return model
211
211
212
212
213 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
213 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
214 """static files should only be accessible when logged in"""
214 """static files should only be accessible when logged in"""
215
215
216 @web.authenticated
216 @web.authenticated
217 def get(self, path):
217 def get(self, path):
218 if os.path.splitext(path)[1] == '.ipynb':
218 if os.path.splitext(path)[1] == '.ipynb':
219 name = os.path.basename(path)
219 name = os.path.basename(path)
220 self.set_header('Content-Type', 'application/json')
220 self.set_header('Content-Type', 'application/json')
221 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
221 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
222
222
223 return web.StaticFileHandler.get(self, path)
223 return web.StaticFileHandler.get(self, path)
224
224
225 def compute_etag(self):
225 def compute_etag(self):
226 return None
226 return None
227
227
228 def validate_absolute_path(self, root, absolute_path):
228 def validate_absolute_path(self, root, absolute_path):
229 """Validate and return the absolute path.
229 """Validate and return the absolute path.
230
230
231 Requires tornado 3.1
231 Requires tornado 3.1
232
232
233 Adding to tornado's own handling, forbids the serving of hidden files.
233 Adding to tornado's own handling, forbids the serving of hidden files.
234 """
234 """
235 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
235 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
236 abs_root = os.path.abspath(root)
236 abs_root = os.path.abspath(root)
237 self.forbid_hidden(abs_root, abs_path)
237 self.forbid_hidden(abs_root, abs_path)
238 return abs_path
238 return abs_path
239
239
240 def forbid_hidden(self, absolute_root, absolute_path):
240 def forbid_hidden(self, absolute_root, absolute_path):
241 """Raise 403 if a file is hidden or contained in a hidden directory.
241 """Raise 403 if a file is hidden or contained in a hidden directory.
242
242
243 Hidden is determined by either name starting with '.'
243 Hidden is determined by either name starting with '.'
244 or the UF_HIDDEN flag as reported by stat
244 or the UF_HIDDEN flag as reported by stat
245 """
245 """
246 inside_root = absolute_path[len(absolute_root):]
246 inside_root = absolute_path[len(absolute_root):]
247 if any(part.startswith('.') for part in inside_root.split(os.sep)):
247 if any(part.startswith('.') for part in inside_root.split(os.sep)):
248 raise web.HTTPError(403)
248 raise web.HTTPError(403)
249
249
250 # check UF_HIDDEN on any location up to root
250 # check UF_HIDDEN on any location up to root
251 path = absolute_path
251 path = absolute_path
252 while path and path.startswith(absolute_root) and path != absolute_root:
252 while path and path.startswith(absolute_root) and path != absolute_root:
253 st = os.stat(path)
253 st = os.stat(path)
254 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
254 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
255 raise web.HTTPError(403)
255 raise web.HTTPError(403)
256 path = os.path.dirname(path)
256 path = os.path.dirname(path)
257
257
258 return absolute_path
258 return absolute_path
259
259
260
260
261 def json_errors(method):
261 def json_errors(method):
262 """Decorate methods with this to return GitHub style JSON errors.
262 """Decorate methods with this to return GitHub style JSON errors.
263
263
264 This should be used on any JSON API on any handler method that can raise HTTPErrors.
264 This should be used on any JSON API on any handler method that can raise HTTPErrors.
265
265
266 This will grab the latest HTTPError exception using sys.exc_info
266 This will grab the latest HTTPError exception using sys.exc_info
267 and then:
267 and then:
268
268
269 1. Set the HTTP status code based on the HTTPError
269 1. Set the HTTP status code based on the HTTPError
270 2. Create and return a JSON body with a message field describing
270 2. Create and return a JSON body with a message field describing
271 the error in a human readable form.
271 the error in a human readable form.
272 """
272 """
273 @functools.wraps(method)
273 @functools.wraps(method)
274 def wrapper(self, *args, **kwargs):
274 def wrapper(self, *args, **kwargs):
275 try:
275 try:
276 result = method(self, *args, **kwargs)
276 result = method(self, *args, **kwargs)
277 except web.HTTPError as e:
277 except web.HTTPError as e:
278 status = e.status_code
278 status = e.status_code
279 message = e.log_message
279 message = e.log_message
280 self.set_status(e.status_code)
280 self.set_status(e.status_code)
281 self.finish(json.dumps(dict(message=message)))
281 self.finish(json.dumps(dict(message=message)))
282 except Exception:
282 except Exception:
283 self.log.error("Unhandled error in API request", exc_info=True)
283 self.log.error("Unhandled error in API request", exc_info=True)
284 status = 500
284 status = 500
285 message = "Unknown server error"
285 message = "Unknown server error"
286 t, value, tb = sys.exc_info()
286 t, value, tb = sys.exc_info()
287 self.set_status(status)
287 self.set_status(status)
288 tb_text = ''.join(traceback.format_exception(t, value, tb))
288 tb_text = ''.join(traceback.format_exception(t, value, tb))
289 reply = dict(message=message, traceback=tb_text)
289 reply = dict(message=message, traceback=tb_text)
290 self.finish(json.dumps(reply))
290 self.finish(json.dumps(reply))
291 else:
291 else:
292 return result
292 return result
293 return wrapper
293 return wrapper
294
294
295
295
296
296
297 #-----------------------------------------------------------------------------
297 #-----------------------------------------------------------------------------
298 # File handler
298 # File handler
299 #-----------------------------------------------------------------------------
299 #-----------------------------------------------------------------------------
300
300
301 # to minimize subclass changes:
301 # to minimize subclass changes:
302 HTTPError = web.HTTPError
302 HTTPError = web.HTTPError
303
303
304 class FileFindHandler(web.StaticFileHandler):
304 class FileFindHandler(web.StaticFileHandler):
305 """subclass of StaticFileHandler for serving files from a search path"""
305 """subclass of StaticFileHandler for serving files from a search path"""
306
306
307 # cache search results, don't search for files more than once
307 # cache search results, don't search for files more than once
308 _static_paths = {}
308 _static_paths = {}
309
309
310 def initialize(self, path, default_filename=None):
310 def initialize(self, path, default_filename=None):
311 if isinstance(path, string_types):
311 if isinstance(path, string_types):
312 path = [path]
312 path = [path]
313
313
314 self.root = tuple(
314 self.root = tuple(
315 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
315 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
316 )
316 )
317 self.default_filename = default_filename
317 self.default_filename = default_filename
318
318
319 def compute_etag(self):
319 def compute_etag(self):
320 return None
320 return None
321
321
322 @classmethod
322 @classmethod
323 def get_absolute_path(cls, roots, path):
323 def get_absolute_path(cls, roots, path):
324 """locate a file to serve on our static file search path"""
324 """locate a file to serve on our static file search path"""
325 with cls._lock:
325 with cls._lock:
326 if path in cls._static_paths:
326 if path in cls._static_paths:
327 return cls._static_paths[path]
327 return cls._static_paths[path]
328 try:
328 try:
329 abspath = os.path.abspath(filefind(path, roots))
329 abspath = os.path.abspath(filefind(path, roots))
330 except IOError:
330 except IOError:
331 # IOError means not found
331 # IOError means not found
332 raise web.HTTPError(404)
332 raise web.HTTPError(404)
333
333
334 cls._static_paths[path] = abspath
334 cls._static_paths[path] = abspath
335 return abspath
335 return abspath
336
336
337 def validate_absolute_path(self, root, absolute_path):
337 def validate_absolute_path(self, root, absolute_path):
338 """check if the file should be served (raises 404, 403, etc.)"""
338 """check if the file should be served (raises 404, 403, etc.)"""
339 for root in self.root:
339 for root in self.root:
340 if (absolute_path + os.sep).startswith(root):
340 if (absolute_path + os.sep).startswith(root):
341 break
341 break
342
342
343 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
343 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
344
344
345
345
346 class TrailingSlashHandler(web.RequestHandler):
346 class TrailingSlashHandler(web.RequestHandler):
347 """Simple redirect handler that strips trailing slashes
347 """Simple redirect handler that strips trailing slashes
348
348
349 This should be the first, highest priority handler.
349 This should be the first, highest priority handler.
350 """
350 """
351
351
352 SUPPORTED_METHODS = ['GET']
352 SUPPORTED_METHODS = ['GET']
353
353
354 def get(self):
354 def get(self):
355 self.redirect(self.request.uri.rstrip('/'))
355 self.redirect(self.request.uri.rstrip('/'))
356
356
357 #-----------------------------------------------------------------------------
357 #-----------------------------------------------------------------------------
358 # URL pattern fragments for re-use
359 #-----------------------------------------------------------------------------
360
361 path_regex = r"(?P<path>(?:/.*)*)"
362 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
363 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
364
365 #-----------------------------------------------------------------------------
358 # URL to handler mappings
366 # URL to handler mappings
359 #-----------------------------------------------------------------------------
367 #-----------------------------------------------------------------------------
360
368
361
369
362 default_handlers = [
370 default_handlers = [
363 (r".*/", TrailingSlashHandler)
371 (r".*/", TrailingSlashHandler)
364 ]
372 ]
@@ -1,85 +1,83 b''
1 import os
1 import os
2
2
3 from tornado import web
3 from tornado import web
4
4
5 from ..base.handlers import IPythonHandler
5 from ..base.handlers import IPythonHandler, notebook_path_regex
6 from IPython.nbformat.current import to_notebook_json
6 from IPython.nbformat.current import to_notebook_json
7 from IPython.nbconvert.exporters.export import exporter_map
7 from IPython.nbconvert.exporters.export import exporter_map
8 from IPython.utils import tz
8 from IPython.utils import tz
9
9
10
10
11 def has_resource_files(resources):
11 def has_resource_files(resources):
12 output_files_dir = resources.get('output_files_dir', "")
12 output_files_dir = resources.get('output_files_dir', "")
13 return bool(os.path.isdir(output_files_dir) and \
13 return bool(os.path.isdir(output_files_dir) and \
14 os.listdir(output_files_dir))
14 os.listdir(output_files_dir))
15
15
16 class NbconvertFileHandler(IPythonHandler):
16 class NbconvertFileHandler(IPythonHandler):
17
17
18 SUPPORTED_METHODS = ('GET',)
18 SUPPORTED_METHODS = ('GET',)
19
19
20 @web.authenticated
20 @web.authenticated
21 def get(self, format, path='', name=None):
21 def get(self, format, path='', name=None):
22 exporter = exporter_map[format](config=self.config)
22 exporter = exporter_map[format](config=self.config)
23
23
24 path = path.strip('/')
24 path = path.strip('/')
25 os_path = self.notebook_manager.get_os_path(name, path)
25 os_path = self.notebook_manager.get_os_path(name, path)
26 if not os.path.isfile(os_path):
26 if not os.path.isfile(os_path):
27 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
27 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
28
28
29 info = os.stat(os_path)
29 info = os.stat(os_path)
30 self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime))
30 self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime))
31
31
32 # Force download if requested
32 # Force download if requested
33 if self.get_argument('download', 'false').lower() == 'true':
33 if self.get_argument('download', 'false').lower() == 'true':
34 filename = os.path.splitext(name)[0] + '.' + exporter.file_extension
34 filename = os.path.splitext(name)[0] + '.' + exporter.file_extension
35 self.set_header('Content-Disposition',
35 self.set_header('Content-Disposition',
36 'attachment; filename="%s"' % filename)
36 'attachment; filename="%s"' % filename)
37
37
38 # MIME type
38 # MIME type
39 if exporter.output_mimetype:
39 if exporter.output_mimetype:
40 self.set_header('Content-Type',
40 self.set_header('Content-Type',
41 '%s; charset=utf-8' % exporter.output_mimetype)
41 '%s; charset=utf-8' % exporter.output_mimetype)
42
42
43 output, resources = exporter.from_filename(os_path)
43 output, resources = exporter.from_filename(os_path)
44
44
45 # TODO: If there are resources, combine them into a zip file
45 # TODO: If there are resources, combine them into a zip file
46 assert not has_resource_files(resources)
46 assert not has_resource_files(resources)
47
47
48 self.finish(output)
48 self.finish(output)
49
49
50 class NbconvertPostHandler(IPythonHandler):
50 class NbconvertPostHandler(IPythonHandler):
51 SUPPORTED_METHODS = ('POST',)
51 SUPPORTED_METHODS = ('POST',)
52
52
53 @web.authenticated
53 @web.authenticated
54 def post(self, format):
54 def post(self, format):
55 exporter = exporter_map[format](config=self.config)
55 exporter = exporter_map[format](config=self.config)
56
56
57 model = self.get_json_body()
57 model = self.get_json_body()
58 nbnode = to_notebook_json(model['content'])
58 nbnode = to_notebook_json(model['content'])
59
59
60 # MIME type
60 # MIME type
61 if exporter.output_mimetype:
61 if exporter.output_mimetype:
62 self.set_header('Content-Type',
62 self.set_header('Content-Type',
63 '%s; charset=utf-8' % exporter.output_mimetype)
63 '%s; charset=utf-8' % exporter.output_mimetype)
64
64
65 output, resources = exporter.from_notebook_node(nbnode)
65 output, resources = exporter.from_notebook_node(nbnode)
66
66
67 # TODO: If there are resources, combine them into a zip file
67 # TODO: If there are resources, combine them into a zip file
68 assert not has_resource_files(resources)
68 assert not has_resource_files(resources)
69
69
70 self.finish(output)
70 self.finish(output)
71
71
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73 # URL to handler mappings
73 # URL to handler mappings
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75
75
76 _format_regex = r"(?P<format>\w+)"
76 _format_regex = r"(?P<format>\w+)"
77 _path_regex = r"(?P<path>(?:/.*)*)"
77
78 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
79 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
80
78
81 default_handlers = [
79 default_handlers = [
82 (r"/nbconvert/%s%s" % (_format_regex, _notebook_path_regex),
80 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
83 NbconvertFileHandler),
81 NbconvertFileHandler),
84 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
82 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
85 ] No newline at end of file
83 ]
@@ -1,91 +1,90 b''
1 """Tornado handlers for the live notebook view.
1 """Tornado handlers for the live notebook view.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 from tornado import web
20 from tornado import web
21 HTTPError = web.HTTPError
21 HTTPError = web.HTTPError
22
22
23 from ..base.handlers import IPythonHandler
23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
24 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
25 from ..utils import url_path_join, url_escape
24 from ..utils import url_path_join, url_escape
26
25
27 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
28 # Handlers
27 # Handlers
29 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
30
29
31
30
32 class NotebookHandler(IPythonHandler):
31 class NotebookHandler(IPythonHandler):
33
32
34 @web.authenticated
33 @web.authenticated
35 def get(self, path='', name=None):
34 def get(self, path='', name=None):
36 """get renders the notebook template if a name is given, or
35 """get renders the notebook template if a name is given, or
37 redirects to the '/files/' handler if the name is not given."""
36 redirects to the '/files/' handler if the name is not given."""
38 path = path.strip('/')
37 path = path.strip('/')
39 nbm = self.notebook_manager
38 nbm = self.notebook_manager
40 if name is None:
39 if name is None:
41 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
40 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
42
41
43 # a .ipynb filename was given
42 # a .ipynb filename was given
44 if not nbm.notebook_exists(name, path):
43 if not nbm.notebook_exists(name, path):
45 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
44 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
46 name = url_escape(name)
45 name = url_escape(name)
47 path = url_escape(path)
46 path = url_escape(path)
48 self.write(self.render_template('notebook.html',
47 self.write(self.render_template('notebook.html',
49 project=self.project_dir,
48 project=self.project_dir,
50 notebook_path=path,
49 notebook_path=path,
51 notebook_name=name,
50 notebook_name=name,
52 kill_kernel=False,
51 kill_kernel=False,
53 mathjax_url=self.mathjax_url,
52 mathjax_url=self.mathjax_url,
54 )
53 )
55 )
54 )
56
55
57 class NotebookRedirectHandler(IPythonHandler):
56 class NotebookRedirectHandler(IPythonHandler):
58 def get(self, path=''):
57 def get(self, path=''):
59 nbm = self.notebook_manager
58 nbm = self.notebook_manager
60 if nbm.path_exists(path):
59 if nbm.path_exists(path):
61 # it's a *directory*, redirect to /tree
60 # it's a *directory*, redirect to /tree
62 url = url_path_join(self.base_project_url, 'tree', path)
61 url = url_path_join(self.base_project_url, 'tree', path)
63 else:
62 else:
64 # otherwise, redirect to /files
63 # otherwise, redirect to /files
65 if '/files/' in path:
64 if '/files/' in path:
66 # redirect without files/ iff it would 404
65 # redirect without files/ iff it would 404
67 # this preserves pre-2.0-style 'files/' links
66 # this preserves pre-2.0-style 'files/' links
68 # FIXME: this is hardcoded based on notebook_path,
67 # FIXME: this is hardcoded based on notebook_path,
69 # but so is the files handler itself,
68 # but so is the files handler itself,
70 # so it should work until both are cleaned up.
69 # so it should work until both are cleaned up.
71 parts = path.split('/')
70 parts = path.split('/')
72 files_path = os.path.join(nbm.notebook_dir, *parts)
71 files_path = os.path.join(nbm.notebook_dir, *parts)
73 self.log.warn("filespath: %s", files_path)
72 self.log.warn("filespath: %s", files_path)
74 if not os.path.exists(files_path):
73 if not os.path.exists(files_path):
75 path = path.replace('/files/', '/', 1)
74 path = path.replace('/files/', '/', 1)
76
75
77 url = url_path_join(self.base_project_url, 'files', path)
76 url = url_path_join(self.base_project_url, 'files', path)
78 url = url_escape(url)
77 url = url_escape(url)
79 self.log.debug("Redirecting %s to %s", self.request.path, url)
78 self.log.debug("Redirecting %s to %s", self.request.path, url)
80 self.redirect(url)
79 self.redirect(url)
81
80
82 #-----------------------------------------------------------------------------
81 #-----------------------------------------------------------------------------
83 # URL to handler mappings
82 # URL to handler mappings
84 #-----------------------------------------------------------------------------
83 #-----------------------------------------------------------------------------
85
84
86
85
87 default_handlers = [
86 default_handlers = [
88 (r"/notebooks%s" % _notebook_path_regex, NotebookHandler),
87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
89 (r"/notebooks%s" % _path_regex, NotebookRedirectHandler),
88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
90 ]
89 ]
91
90
@@ -1,281 +1,280 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the notebooks web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from IPython.html.utils import url_path_join, url_escape
23 from IPython.html.utils import url_path_join, url_escape
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25
25
26 from IPython.html.base.handlers import IPythonHandler, json_errors
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 notebook_path_regex, path_regex,
28 notebook_name_regex)
27
29
28 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
29 # Notebook web service handlers
31 # Notebook web service handlers
30 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
31
33
32
34
33 class NotebookHandler(IPythonHandler):
35 class NotebookHandler(IPythonHandler):
34
36
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36
38
37 def notebook_location(self, name, path=''):
39 def notebook_location(self, name, path=''):
38 """Return the full URL location of a notebook based.
40 """Return the full URL location of a notebook based.
39
41
40 Parameters
42 Parameters
41 ----------
43 ----------
42 name : unicode
44 name : unicode
43 The base name of the notebook, such as "foo.ipynb".
45 The base name of the notebook, such as "foo.ipynb".
44 path : unicode
46 path : unicode
45 The URL path of the notebook.
47 The URL path of the notebook.
46 """
48 """
47 return url_escape(url_path_join(
49 return url_escape(url_path_join(
48 self.base_project_url, 'api', 'notebooks', path, name
50 self.base_project_url, 'api', 'notebooks', path, name
49 ))
51 ))
50
52
51 def _finish_model(self, model, location=True):
53 def _finish_model(self, model, location=True):
52 """Finish a JSON request with a model, setting relevant headers, etc."""
54 """Finish a JSON request with a model, setting relevant headers, etc."""
53 if location:
55 if location:
54 location = self.notebook_location(model['name'], model['path'])
56 location = self.notebook_location(model['name'], model['path'])
55 self.set_header('Location', location)
57 self.set_header('Location', location)
56 self.set_header('Last-Modified', model['last_modified'])
58 self.set_header('Last-Modified', model['last_modified'])
57 self.finish(json.dumps(model, default=date_default))
59 self.finish(json.dumps(model, default=date_default))
58
60
59 @web.authenticated
61 @web.authenticated
60 @json_errors
62 @json_errors
61 def get(self, path='', name=None):
63 def get(self, path='', name=None):
62 """Return a Notebook or list of notebooks.
64 """Return a Notebook or list of notebooks.
63
65
64 * GET with path and no notebook name lists notebooks in a directory
66 * GET with path and no notebook name lists notebooks in a directory
65 * GET with path and notebook name returns notebook JSON
67 * GET with path and notebook name returns notebook JSON
66 """
68 """
67 nbm = self.notebook_manager
69 nbm = self.notebook_manager
68 # Check to see if a notebook name was given
70 # Check to see if a notebook name was given
69 if name is None:
71 if name is None:
70 # List notebooks in 'path'
72 # List notebooks in 'path'
71 notebooks = nbm.list_notebooks(path)
73 notebooks = nbm.list_notebooks(path)
72 self.finish(json.dumps(notebooks, default=date_default))
74 self.finish(json.dumps(notebooks, default=date_default))
73 return
75 return
74 # get and return notebook representation
76 # get and return notebook representation
75 model = nbm.get_notebook_model(name, path)
77 model = nbm.get_notebook_model(name, path)
76 self._finish_model(model, location=False)
78 self._finish_model(model, location=False)
77
79
78 @web.authenticated
80 @web.authenticated
79 @json_errors
81 @json_errors
80 def patch(self, path='', name=None):
82 def patch(self, path='', name=None):
81 """PATCH renames a notebook without re-uploading content."""
83 """PATCH renames a notebook without re-uploading content."""
82 nbm = self.notebook_manager
84 nbm = self.notebook_manager
83 if name is None:
85 if name is None:
84 raise web.HTTPError(400, u'Notebook name missing')
86 raise web.HTTPError(400, u'Notebook name missing')
85 model = self.get_json_body()
87 model = self.get_json_body()
86 if model is None:
88 if model is None:
87 raise web.HTTPError(400, u'JSON body missing')
89 raise web.HTTPError(400, u'JSON body missing')
88 model = nbm.update_notebook_model(model, name, path)
90 model = nbm.update_notebook_model(model, name, path)
89 self._finish_model(model)
91 self._finish_model(model)
90
92
91 def _copy_notebook(self, copy_from, path, copy_to=None):
93 def _copy_notebook(self, copy_from, path, copy_to=None):
92 """Copy a notebook in path, optionally specifying the new name.
94 """Copy a notebook in path, optionally specifying the new name.
93
95
94 Only support copying within the same directory.
96 Only support copying within the same directory.
95 """
97 """
96 self.log.info(u"Copying notebook from %s/%s to %s/%s",
98 self.log.info(u"Copying notebook from %s/%s to %s/%s",
97 path, copy_from,
99 path, copy_from,
98 path, copy_to or '',
100 path, copy_to or '',
99 )
101 )
100 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
102 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
101 self.set_status(201)
103 self.set_status(201)
102 self._finish_model(model)
104 self._finish_model(model)
103
105
104 def _upload_notebook(self, model, path, name=None):
106 def _upload_notebook(self, model, path, name=None):
105 """Upload a notebook
107 """Upload a notebook
106
108
107 If name specified, create it in path/name.
109 If name specified, create it in path/name.
108 """
110 """
109 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
111 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
110 if name:
112 if name:
111 model['name'] = name
113 model['name'] = name
112
114
113 model = self.notebook_manager.create_notebook_model(model, path)
115 model = self.notebook_manager.create_notebook_model(model, path)
114 self.set_status(201)
116 self.set_status(201)
115 self._finish_model(model)
117 self._finish_model(model)
116
118
117 def _create_empty_notebook(self, path, name=None):
119 def _create_empty_notebook(self, path, name=None):
118 """Create an empty notebook in path
120 """Create an empty notebook in path
119
121
120 If name specified, create it in path/name.
122 If name specified, create it in path/name.
121 """
123 """
122 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
124 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
123 model = {}
125 model = {}
124 if name:
126 if name:
125 model['name'] = name
127 model['name'] = name
126 model = self.notebook_manager.create_notebook_model(model, path=path)
128 model = self.notebook_manager.create_notebook_model(model, path=path)
127 self.set_status(201)
129 self.set_status(201)
128 self._finish_model(model)
130 self._finish_model(model)
129
131
130 def _save_notebook(self, model, path, name):
132 def _save_notebook(self, model, path, name):
131 """Save an existing notebook."""
133 """Save an existing notebook."""
132 self.log.info(u"Saving notebook at %s/%s", path, name)
134 self.log.info(u"Saving notebook at %s/%s", path, name)
133 model = self.notebook_manager.save_notebook_model(model, name, path)
135 model = self.notebook_manager.save_notebook_model(model, name, path)
134 if model['path'] != path.strip('/') or model['name'] != name:
136 if model['path'] != path.strip('/') or model['name'] != name:
135 # a rename happened, set Location header
137 # a rename happened, set Location header
136 location = True
138 location = True
137 else:
139 else:
138 location = False
140 location = False
139 self._finish_model(model, location)
141 self._finish_model(model, location)
140
142
141 @web.authenticated
143 @web.authenticated
142 @json_errors
144 @json_errors
143 def post(self, path='', name=None):
145 def post(self, path='', name=None):
144 """Create a new notebook in the specified path.
146 """Create a new notebook in the specified path.
145
147
146 POST creates new notebooks. The server always decides on the notebook name.
148 POST creates new notebooks. The server always decides on the notebook name.
147
149
148 POST /api/notebooks/path
150 POST /api/notebooks/path
149 New untitled notebook in path. If content specified, upload a
151 New untitled notebook in path. If content specified, upload a
150 notebook, otherwise start empty.
152 notebook, otherwise start empty.
151 POST /api/notebooks/path?copy=OtherNotebook.ipynb
153 POST /api/notebooks/path?copy=OtherNotebook.ipynb
152 New copy of OtherNotebook in path
154 New copy of OtherNotebook in path
153 """
155 """
154
156
155 if name is not None:
157 if name is not None:
156 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
158 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
157
159
158 model = self.get_json_body()
160 model = self.get_json_body()
159
161
160 if model is not None:
162 if model is not None:
161 copy_from = model.get('copy_from')
163 copy_from = model.get('copy_from')
162 if copy_from:
164 if copy_from:
163 if model.get('content'):
165 if model.get('content'):
164 raise web.HTTPError(400, "Can't upload and copy at the same time.")
166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
165 self._copy_notebook(copy_from, path)
167 self._copy_notebook(copy_from, path)
166 else:
168 else:
167 self._upload_notebook(model, path)
169 self._upload_notebook(model, path)
168 else:
170 else:
169 self._create_empty_notebook(path)
171 self._create_empty_notebook(path)
170
172
171 @web.authenticated
173 @web.authenticated
172 @json_errors
174 @json_errors
173 def put(self, path='', name=None):
175 def put(self, path='', name=None):
174 """Saves the notebook in the location specified by name and path.
176 """Saves the notebook in the location specified by name and path.
175
177
176 PUT is very similar to POST, but the requester specifies the name,
178 PUT is very similar to POST, but the requester specifies the name,
177 whereas with POST, the server picks the name.
179 whereas with POST, the server picks the name.
178
180
179 PUT /api/notebooks/path/Name.ipynb
181 PUT /api/notebooks/path/Name.ipynb
180 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
182 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
181 in `content` key of JSON request body. If content is not specified,
183 in `content` key of JSON request body. If content is not specified,
182 create a new empty notebook.
184 create a new empty notebook.
183 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
185 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
184 Copy OtherNotebook to Name
186 Copy OtherNotebook to Name
185 """
187 """
186 if name is None:
188 if name is None:
187 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
189 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
188
190
189 model = self.get_json_body()
191 model = self.get_json_body()
190 if model:
192 if model:
191 copy_from = model.get('copy_from')
193 copy_from = model.get('copy_from')
192 if copy_from:
194 if copy_from:
193 if model.get('content'):
195 if model.get('content'):
194 raise web.HTTPError(400, "Can't upload and copy at the same time.")
196 raise web.HTTPError(400, "Can't upload and copy at the same time.")
195 self._copy_notebook(copy_from, path, name)
197 self._copy_notebook(copy_from, path, name)
196 elif self.notebook_manager.notebook_exists(name, path):
198 elif self.notebook_manager.notebook_exists(name, path):
197 self._save_notebook(model, path, name)
199 self._save_notebook(model, path, name)
198 else:
200 else:
199 self._upload_notebook(model, path, name)
201 self._upload_notebook(model, path, name)
200 else:
202 else:
201 self._create_empty_notebook(path, name)
203 self._create_empty_notebook(path, name)
202
204
203 @web.authenticated
205 @web.authenticated
204 @json_errors
206 @json_errors
205 def delete(self, path='', name=None):
207 def delete(self, path='', name=None):
206 """delete the notebook in the given notebook path"""
208 """delete the notebook in the given notebook path"""
207 nbm = self.notebook_manager
209 nbm = self.notebook_manager
208 nbm.delete_notebook_model(name, path)
210 nbm.delete_notebook_model(name, path)
209 self.set_status(204)
211 self.set_status(204)
210 self.finish()
212 self.finish()
211
213
212
214
213 class NotebookCheckpointsHandler(IPythonHandler):
215 class NotebookCheckpointsHandler(IPythonHandler):
214
216
215 SUPPORTED_METHODS = ('GET', 'POST')
217 SUPPORTED_METHODS = ('GET', 'POST')
216
218
217 @web.authenticated
219 @web.authenticated
218 @json_errors
220 @json_errors
219 def get(self, path='', name=None):
221 def get(self, path='', name=None):
220 """get lists checkpoints for a notebook"""
222 """get lists checkpoints for a notebook"""
221 nbm = self.notebook_manager
223 nbm = self.notebook_manager
222 checkpoints = nbm.list_checkpoints(name, path)
224 checkpoints = nbm.list_checkpoints(name, path)
223 data = json.dumps(checkpoints, default=date_default)
225 data = json.dumps(checkpoints, default=date_default)
224 self.finish(data)
226 self.finish(data)
225
227
226 @web.authenticated
228 @web.authenticated
227 @json_errors
229 @json_errors
228 def post(self, path='', name=None):
230 def post(self, path='', name=None):
229 """post creates a new checkpoint"""
231 """post creates a new checkpoint"""
230 nbm = self.notebook_manager
232 nbm = self.notebook_manager
231 checkpoint = nbm.create_checkpoint(name, path)
233 checkpoint = nbm.create_checkpoint(name, path)
232 data = json.dumps(checkpoint, default=date_default)
234 data = json.dumps(checkpoint, default=date_default)
233 location = url_path_join(self.base_project_url, 'api/notebooks',
235 location = url_path_join(self.base_project_url, 'api/notebooks',
234 path, name, 'checkpoints', checkpoint['id'])
236 path, name, 'checkpoints', checkpoint['id'])
235 self.set_header('Location', url_escape(location))
237 self.set_header('Location', url_escape(location))
236 self.set_status(201)
238 self.set_status(201)
237 self.finish(data)
239 self.finish(data)
238
240
239
241
240 class ModifyNotebookCheckpointsHandler(IPythonHandler):
242 class ModifyNotebookCheckpointsHandler(IPythonHandler):
241
243
242 SUPPORTED_METHODS = ('POST', 'DELETE')
244 SUPPORTED_METHODS = ('POST', 'DELETE')
243
245
244 @web.authenticated
246 @web.authenticated
245 @json_errors
247 @json_errors
246 def post(self, path, name, checkpoint_id):
248 def post(self, path, name, checkpoint_id):
247 """post restores a notebook from a checkpoint"""
249 """post restores a notebook from a checkpoint"""
248 nbm = self.notebook_manager
250 nbm = self.notebook_manager
249 nbm.restore_checkpoint(checkpoint_id, name, path)
251 nbm.restore_checkpoint(checkpoint_id, name, path)
250 self.set_status(204)
252 self.set_status(204)
251 self.finish()
253 self.finish()
252
254
253 @web.authenticated
255 @web.authenticated
254 @json_errors
256 @json_errors
255 def delete(self, path, name, checkpoint_id):
257 def delete(self, path, name, checkpoint_id):
256 """delete clears a checkpoint for a given notebook"""
258 """delete clears a checkpoint for a given notebook"""
257 nbm = self.notebook_manager
259 nbm = self.notebook_manager
258 nbm.delete_checkpoint(checkpoint_id, name, path)
260 nbm.delete_checkpoint(checkpoint_id, name, path)
259 self.set_status(204)
261 self.set_status(204)
260 self.finish()
262 self.finish()
261
263
262 #-----------------------------------------------------------------------------
264 #-----------------------------------------------------------------------------
263 # URL to handler mappings
265 # URL to handler mappings
264 #-----------------------------------------------------------------------------
266 #-----------------------------------------------------------------------------
265
267
266
268
267 _path_regex = r"(?P<path>(?:/.*)*)"
268 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
269 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
269 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
270 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
271
270
272 default_handlers = [
271 default_handlers = [
273 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
272 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
274 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
273 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
275 ModifyNotebookCheckpointsHandler),
274 ModifyNotebookCheckpointsHandler),
276 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
275 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
277 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
276 (r"/api/notebooks%s" % path_regex, NotebookHandler),
278 ]
277 ]
279
278
280
279
281
280
@@ -1,77 +1,76 b''
1 """Tornado handlers for the tree view.
1 """Tornado handlers for the tree view.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import os
18 import os
19
19
20 from tornado import web
20 from tornado import web
21 from ..base.handlers import IPythonHandler
21 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
22 from ..utils import url_path_join, path2url, url2path, url_escape
22 from ..utils import url_path_join, path2url, url2path, url_escape
23 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
24
23
25 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
26 # Handlers
25 # Handlers
27 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
28
27
29
28
30 class TreeHandler(IPythonHandler):
29 class TreeHandler(IPythonHandler):
31 """Render the tree view, listing notebooks, clusters, etc."""
30 """Render the tree view, listing notebooks, clusters, etc."""
32
31
33 @web.authenticated
32 @web.authenticated
34 def get(self, path='', name=None):
33 def get(self, path='', name=None):
35 path = path.strip('/')
34 path = path.strip('/')
36 nbm = self.notebook_manager
35 nbm = self.notebook_manager
37 if name is not None:
36 if name is not None:
38 # is a notebook, redirect to notebook handler
37 # is a notebook, redirect to notebook handler
39 url = url_escape(url_path_join(
38 url = url_escape(url_path_join(
40 self.base_project_url, 'notebooks', path, name
39 self.base_project_url, 'notebooks', path, name
41 ))
40 ))
42 self.log.debug("Redirecting %s to %s", self.request.path, url)
41 self.log.debug("Redirecting %s to %s", self.request.path, url)
43 self.redirect(url)
42 self.redirect(url)
44 else:
43 else:
45 if not nbm.path_exists(path=path):
44 if not nbm.path_exists(path=path):
46 # no such directory, 404
45 # no such directory, 404
47 raise web.HTTPError(404)
46 raise web.HTTPError(404)
48 self.write(self.render_template('tree.html',
47 self.write(self.render_template('tree.html',
49 project=self.project_dir,
48 project=self.project_dir,
50 tree_url_path=path,
49 tree_url_path=path,
51 notebook_path=path,
50 notebook_path=path,
52 ))
51 ))
53
52
54
53
55 class TreeRedirectHandler(IPythonHandler):
54 class TreeRedirectHandler(IPythonHandler):
56 """Redirect a request to the corresponding tree URL"""
55 """Redirect a request to the corresponding tree URL"""
57
56
58 @web.authenticated
57 @web.authenticated
59 def get(self, path=''):
58 def get(self, path=''):
60 url = url_escape(url_path_join(
59 url = url_escape(url_path_join(
61 self.base_project_url, 'tree', path.strip('/')
60 self.base_project_url, 'tree', path.strip('/')
62 ))
61 ))
63 self.log.debug("Redirecting %s to %s", self.request.path, url)
62 self.log.debug("Redirecting %s to %s", self.request.path, url)
64 self.redirect(url)
63 self.redirect(url)
65
64
66
65
67 #-----------------------------------------------------------------------------
66 #-----------------------------------------------------------------------------
68 # URL to handler mappings
67 # URL to handler mappings
69 #-----------------------------------------------------------------------------
68 #-----------------------------------------------------------------------------
70
69
71
70
72 default_handlers = [
71 default_handlers = [
73 (r"/tree%s" % _notebook_path_regex, TreeHandler),
72 (r"/tree%s" % notebook_path_regex, TreeHandler),
74 (r"/tree%s" % _path_regex, TreeHandler),
73 (r"/tree%s" % path_regex, TreeHandler),
75 (r"/tree", TreeHandler),
74 (r"/tree", TreeHandler),
76 (r"/", TreeRedirectHandler),
75 (r"/", TreeRedirectHandler),
77 ]
76 ]
General Comments 0
You need to be logged in to leave comments. Login now