##// END OF EJS Templates
Handle notebook downloads through the /files URL.
Brian E. Granger -
Show More
@@ -1,492 +1,491
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 datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import functools
22 import functools
23 import hashlib
23 import hashlib
24 import json
24 import json
25 import logging
25 import logging
26 import mimetypes
26 import mimetypes
27 import os
27 import os
28 import stat
28 import stat
29 import sys
29 import sys
30 import threading
30 import threading
31 import traceback
31 import traceback
32
32
33 from tornado import web
33 from tornado import web
34 from tornado import websocket
34 from tornado import websocket
35
35
36 try:
36 try:
37 from tornado.log import app_log
37 from tornado.log import app_log
38 except ImportError:
38 except ImportError:
39 app_log = logging.getLogger()
39 app_log = logging.getLogger()
40
40
41 from IPython.config import Application
41 from IPython.config import Application
42 from IPython.external.decorator import decorator
42 from IPython.external.decorator import decorator
43 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44 from IPython.utils.jsonutil import date_default
44 from IPython.utils.jsonutil import date_default
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
47 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49
49
50 # Google Chrome, as of release 16, changed its websocket protocol number. The
50 # Google Chrome, as of release 16, changed its websocket protocol number. The
51 # parts tornado cares about haven't really changed, so it's OK to continue
51 # parts tornado cares about haven't really changed, so it's OK to continue
52 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
52 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
53 # version as of Oct 30/2011) the version check fails, see the issue report:
53 # version as of Oct 30/2011) the version check fails, see the issue report:
54
54
55 # https://github.com/facebook/tornado/issues/385
55 # https://github.com/facebook/tornado/issues/385
56
56
57 # This issue has been fixed in Tornado post 2.1.1:
57 # This issue has been fixed in Tornado post 2.1.1:
58
58
59 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
59 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
60
60
61 # Here we manually apply the same patch as above so that users of IPython can
61 # Here we manually apply the same patch as above so that users of IPython can
62 # continue to work with an officially released Tornado. We make the
62 # continue to work with an officially released Tornado. We make the
63 # monkeypatch version check as narrow as possible to limit its effects; once
63 # monkeypatch version check as narrow as possible to limit its effects; once
64 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
64 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
65
65
66 import tornado
66 import tornado
67
67
68 if tornado.version_info <= (2,1,1):
68 if tornado.version_info <= (2,1,1):
69
69
70 def _execute(self, transforms, *args, **kwargs):
70 def _execute(self, transforms, *args, **kwargs):
71 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
71 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
72
72
73 self.open_args = args
73 self.open_args = args
74 self.open_kwargs = kwargs
74 self.open_kwargs = kwargs
75
75
76 # The difference between version 8 and 13 is that in 8 the
76 # The difference between version 8 and 13 is that in 8 the
77 # client sends a "Sec-Websocket-Origin" header and in 13 it's
77 # client sends a "Sec-Websocket-Origin" header and in 13 it's
78 # simply "Origin".
78 # simply "Origin".
79 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
79 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
80 self.ws_connection = WebSocketProtocol8(self)
80 self.ws_connection = WebSocketProtocol8(self)
81 self.ws_connection.accept_connection()
81 self.ws_connection.accept_connection()
82
82
83 elif self.request.headers.get("Sec-WebSocket-Version"):
83 elif self.request.headers.get("Sec-WebSocket-Version"):
84 self.stream.write(tornado.escape.utf8(
84 self.stream.write(tornado.escape.utf8(
85 "HTTP/1.1 426 Upgrade Required\r\n"
85 "HTTP/1.1 426 Upgrade Required\r\n"
86 "Sec-WebSocket-Version: 8\r\n\r\n"))
86 "Sec-WebSocket-Version: 8\r\n\r\n"))
87 self.stream.close()
87 self.stream.close()
88
88
89 else:
89 else:
90 self.ws_connection = WebSocketProtocol76(self)
90 self.ws_connection = WebSocketProtocol76(self)
91 self.ws_connection.accept_connection()
91 self.ws_connection.accept_connection()
92
92
93 websocket.WebSocketHandler._execute = _execute
93 websocket.WebSocketHandler._execute = _execute
94 del _execute
94 del _execute
95
95
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Top-level handlers
98 # Top-level handlers
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 class RequestHandler(web.RequestHandler):
101 class RequestHandler(web.RequestHandler):
102 """RequestHandler with default variable setting."""
102 """RequestHandler with default variable setting."""
103
103
104 def render(*args, **kwargs):
104 def render(*args, **kwargs):
105 kwargs.setdefault('message', '')
105 kwargs.setdefault('message', '')
106 return web.RequestHandler.render(*args, **kwargs)
106 return web.RequestHandler.render(*args, **kwargs)
107
107
108 class AuthenticatedHandler(RequestHandler):
108 class AuthenticatedHandler(RequestHandler):
109 """A RequestHandler with an authenticated user."""
109 """A RequestHandler with an authenticated user."""
110
110
111 def clear_login_cookie(self):
111 def clear_login_cookie(self):
112 self.clear_cookie(self.cookie_name)
112 self.clear_cookie(self.cookie_name)
113
113
114 def get_current_user(self):
114 def get_current_user(self):
115 user_id = self.get_secure_cookie(self.cookie_name)
115 user_id = self.get_secure_cookie(self.cookie_name)
116 # For now the user_id should not return empty, but it could eventually
116 # For now the user_id should not return empty, but it could eventually
117 if user_id == '':
117 if user_id == '':
118 user_id = 'anonymous'
118 user_id = 'anonymous'
119 if user_id is None:
119 if user_id is None:
120 # prevent extra Invalid cookie sig warnings:
120 # prevent extra Invalid cookie sig warnings:
121 self.clear_login_cookie()
121 self.clear_login_cookie()
122 if not self.login_available:
122 if not self.login_available:
123 user_id = 'anonymous'
123 user_id = 'anonymous'
124 return user_id
124 return user_id
125
125
126 @property
126 @property
127 def cookie_name(self):
127 def cookie_name(self):
128 default_cookie_name = 'username-{host}'.format(
128 default_cookie_name = 'username-{host}'.format(
129 host=self.request.host,
129 host=self.request.host,
130 ).replace(':', '-')
130 ).replace(':', '-')
131 return self.settings.get('cookie_name', default_cookie_name)
131 return self.settings.get('cookie_name', default_cookie_name)
132
132
133 @property
133 @property
134 def password(self):
134 def password(self):
135 """our password"""
135 """our password"""
136 return self.settings.get('password', '')
136 return self.settings.get('password', '')
137
137
138 @property
138 @property
139 def logged_in(self):
139 def logged_in(self):
140 """Is a user currently logged in?
140 """Is a user currently logged in?
141
141
142 """
142 """
143 user = self.get_current_user()
143 user = self.get_current_user()
144 return (user and not user == 'anonymous')
144 return (user and not user == 'anonymous')
145
145
146 @property
146 @property
147 def login_available(self):
147 def login_available(self):
148 """May a user proceed to log in?
148 """May a user proceed to log in?
149
149
150 This returns True if login capability is available, irrespective of
150 This returns True if login capability is available, irrespective of
151 whether the user is already logged in or not.
151 whether the user is already logged in or not.
152
152
153 """
153 """
154 return bool(self.settings.get('password', ''))
154 return bool(self.settings.get('password', ''))
155
155
156
156
157 class IPythonHandler(AuthenticatedHandler):
157 class IPythonHandler(AuthenticatedHandler):
158 """IPython-specific extensions to authenticated handling
158 """IPython-specific extensions to authenticated handling
159
159
160 Mostly property shortcuts to IPython-specific settings.
160 Mostly property shortcuts to IPython-specific settings.
161 """
161 """
162
162
163 @property
163 @property
164 def config(self):
164 def config(self):
165 return self.settings.get('config', None)
165 return self.settings.get('config', None)
166
166
167 @property
167 @property
168 def log(self):
168 def log(self):
169 """use the IPython log by default, falling back on tornado's logger"""
169 """use the IPython log by default, falling back on tornado's logger"""
170 if Application.initialized():
170 if Application.initialized():
171 return Application.instance().log
171 return Application.instance().log
172 else:
172 else:
173 return app_log
173 return app_log
174
174
175 @property
175 @property
176 def use_less(self):
176 def use_less(self):
177 """Use less instead of css in templates"""
177 """Use less instead of css in templates"""
178 return self.settings.get('use_less', False)
178 return self.settings.get('use_less', False)
179
179
180 #---------------------------------------------------------------
180 #---------------------------------------------------------------
181 # URLs
181 # URLs
182 #---------------------------------------------------------------
182 #---------------------------------------------------------------
183
183
184 @property
184 @property
185 def ws_url(self):
185 def ws_url(self):
186 """websocket url matching the current request
186 """websocket url matching the current request
187
187
188 By default, this is just `''`, indicating that it should match
188 By default, this is just `''`, indicating that it should match
189 the same host, protocol, port, etc.
189 the same host, protocol, port, etc.
190 """
190 """
191 return self.settings.get('websocket_url', '')
191 return self.settings.get('websocket_url', '')
192
192
193 @property
193 @property
194 def mathjax_url(self):
194 def mathjax_url(self):
195 return self.settings.get('mathjax_url', '')
195 return self.settings.get('mathjax_url', '')
196
196
197 @property
197 @property
198 def base_project_url(self):
198 def base_project_url(self):
199 return self.settings.get('base_project_url', '/')
199 return self.settings.get('base_project_url', '/')
200
200
201 @property
201 @property
202 def base_kernel_url(self):
202 def base_kernel_url(self):
203 return self.settings.get('base_kernel_url', '/')
203 return self.settings.get('base_kernel_url', '/')
204
204
205 #---------------------------------------------------------------
205 #---------------------------------------------------------------
206 # Manager objects
206 # Manager objects
207 #---------------------------------------------------------------
207 #---------------------------------------------------------------
208
208
209 @property
209 @property
210 def kernel_manager(self):
210 def kernel_manager(self):
211 return self.settings['kernel_manager']
211 return self.settings['kernel_manager']
212
212
213 @property
213 @property
214 def notebook_manager(self):
214 def notebook_manager(self):
215 return self.settings['notebook_manager']
215 return self.settings['notebook_manager']
216
216
217 @property
217 @property
218 def cluster_manager(self):
218 def cluster_manager(self):
219 return self.settings['cluster_manager']
219 return self.settings['cluster_manager']
220
220
221 @property
221 @property
222 def session_manager(self):
222 def session_manager(self):
223 return self.settings['session_manager']
223 return self.settings['session_manager']
224
224
225 @property
225 @property
226 def project_dir(self):
226 def project_dir(self):
227 return self.notebook_manager.notebook_dir
227 return self.notebook_manager.notebook_dir
228
228
229 #---------------------------------------------------------------
229 #---------------------------------------------------------------
230 # template rendering
230 # template rendering
231 #---------------------------------------------------------------
231 #---------------------------------------------------------------
232
232
233 def get_template(self, name):
233 def get_template(self, name):
234 """Return the jinja template object for a given name"""
234 """Return the jinja template object for a given name"""
235 return self.settings['jinja2_env'].get_template(name)
235 return self.settings['jinja2_env'].get_template(name)
236
236
237 def render_template(self, name, **ns):
237 def render_template(self, name, **ns):
238 ns.update(self.template_namespace)
238 ns.update(self.template_namespace)
239 template = self.get_template(name)
239 template = self.get_template(name)
240 return template.render(**ns)
240 return template.render(**ns)
241
241
242 @property
242 @property
243 def template_namespace(self):
243 def template_namespace(self):
244 return dict(
244 return dict(
245 base_project_url=self.base_project_url,
245 base_project_url=self.base_project_url,
246 base_kernel_url=self.base_kernel_url,
246 base_kernel_url=self.base_kernel_url,
247 logged_in=self.logged_in,
247 logged_in=self.logged_in,
248 login_available=self.login_available,
248 login_available=self.login_available,
249 use_less=self.use_less,
249 use_less=self.use_less,
250 )
250 )
251
251
252 def get_json_body(self):
252 def get_json_body(self):
253 """Return the body of the request as JSON data."""
253 """Return the body of the request as JSON data."""
254 if not self.request.body:
254 if not self.request.body:
255 return None
255 return None
256 # Do we need to call body.decode('utf-8') here?
256 # Do we need to call body.decode('utf-8') here?
257 body = self.request.body.strip().decode(u'utf-8')
257 body = self.request.body.strip().decode(u'utf-8')
258 try:
258 try:
259 model = json.loads(body)
259 model = json.loads(body)
260 except Exception:
260 except Exception:
261 self.log.debug("Bad JSON: %r", body)
261 self.log.debug("Bad JSON: %r", body)
262 self.log.error("Couldn't parse JSON", exc_info=True)
262 self.log.error("Couldn't parse JSON", exc_info=True)
263 raise web.HTTPError(400, u'Invalid JSON in body of request')
263 raise web.HTTPError(400, u'Invalid JSON in body of request')
264 return model
264 return model
265
265
266 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
266 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
267 """static files should only be accessible when logged in"""
267 """static files should only be accessible when logged in"""
268
268
269 @web.authenticated
269 @web.authenticated
270 def get(self, path):
270 def get(self, path):
271 if os.path.splitext(path)[1] == '.ipynb':
272 name = os.path.basename(path)
273 self.set_header('Content-Type', 'application/json')
274 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
271 return web.StaticFileHandler.get(self, path)
275 return web.StaticFileHandler.get(self, path)
272
276
273
277
274 def json_errors(method):
278 def json_errors(method):
275 """Decorate methods with this to return GitHub style JSON errors.
279 """Decorate methods with this to return GitHub style JSON errors.
276
280
277 This should be used on any JSON API on any handler method that can raise HTTPErrors.
281 This should be used on any JSON API on any handler method that can raise HTTPErrors.
278
282
279 This will grab the latest HTTPError exception using sys.exc_info
283 This will grab the latest HTTPError exception using sys.exc_info
280 and then:
284 and then:
281
285
282 1. Set the HTTP status code based on the HTTPError
286 1. Set the HTTP status code based on the HTTPError
283 2. Create and return a JSON body with a message field describing
287 2. Create and return a JSON body with a message field describing
284 the error in a human readable form.
288 the error in a human readable form.
285 """
289 """
286 @functools.wraps(method)
290 @functools.wraps(method)
287 def wrapper(self, *args, **kwargs):
291 def wrapper(self, *args, **kwargs):
288 try:
292 try:
289 result = method(self, *args, **kwargs)
293 result = method(self, *args, **kwargs)
290 except web.HTTPError as e:
294 except web.HTTPError as e:
291 status = e.status_code
295 status = e.status_code
292 message = e.log_message
296 message = e.log_message
293 self.set_status(e.status_code)
297 self.set_status(e.status_code)
294 self.finish(json.dumps(dict(message=message)))
298 self.finish(json.dumps(dict(message=message)))
295 except Exception:
299 except Exception:
296 self.log.error("Unhandled error in API request", exc_info=True)
300 self.log.error("Unhandled error in API request", exc_info=True)
297 status = 500
301 status = 500
298 message = "Unknown server error"
302 message = "Unknown server error"
299 t, value, tb = sys.exc_info()
303 t, value, tb = sys.exc_info()
300 self.set_status(status)
304 self.set_status(status)
301 tb_text = ''.join(traceback.format_exception(t, value, tb))
305 tb_text = ''.join(traceback.format_exception(t, value, tb))
302 reply = dict(message=message, traceback=tb_text)
306 reply = dict(message=message, traceback=tb_text)
303 self.finish(json.dumps(reply))
307 self.finish(json.dumps(reply))
304 else:
308 else:
305 return result
309 return result
306 return wrapper
310 return wrapper
307
311
308
312
309
313
310 #-----------------------------------------------------------------------------
314 #-----------------------------------------------------------------------------
311 # File handler
315 # File handler
312 #-----------------------------------------------------------------------------
316 #-----------------------------------------------------------------------------
313
317
314 # to minimize subclass changes:
318 # to minimize subclass changes:
315 HTTPError = web.HTTPError
319 HTTPError = web.HTTPError
316
320
317 class FileFindHandler(web.StaticFileHandler):
321 class FileFindHandler(web.StaticFileHandler):
318 """subclass of StaticFileHandler for serving files from a search path"""
322 """subclass of StaticFileHandler for serving files from a search path"""
319
323
320 _static_paths = {}
324 _static_paths = {}
321 # _lock is needed for tornado < 2.2.0 compat
325 # _lock is needed for tornado < 2.2.0 compat
322 _lock = threading.Lock() # protects _static_hashes
326 _lock = threading.Lock() # protects _static_hashes
323
327
324 def initialize(self, path, default_filename=None):
328 def initialize(self, path, default_filename=None):
325 if isinstance(path, basestring):
329 if isinstance(path, basestring):
326 path = [path]
330 path = [path]
327 self.roots = tuple(
331 self.roots = tuple(
328 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
332 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
329 )
333 )
330 self.default_filename = default_filename
334 self.default_filename = default_filename
331
335
332 @classmethod
336 @classmethod
333 def locate_file(cls, path, roots):
337 def locate_file(cls, path, roots):
334 """locate a file to serve on our static file search path"""
338 """locate a file to serve on our static file search path"""
335 with cls._lock:
339 with cls._lock:
336 if path in cls._static_paths:
340 if path in cls._static_paths:
337 return cls._static_paths[path]
341 return cls._static_paths[path]
338 try:
342 try:
339 abspath = os.path.abspath(filefind(path, roots))
343 abspath = os.path.abspath(filefind(path, roots))
340 except IOError:
344 except IOError:
341 # empty string should always give exists=False
345 # empty string should always give exists=False
342 return ''
346 return ''
343
347
344 # os.path.abspath strips a trailing /
348 # os.path.abspath strips a trailing /
345 # it needs to be temporarily added back for requests to root/
349 # it needs to be temporarily added back for requests to root/
346 if not (abspath + os.path.sep).startswith(roots):
350 if not (abspath + os.path.sep).startswith(roots):
347 raise HTTPError(403, "%s is not in root static directory", path)
351 raise HTTPError(403, "%s is not in root static directory", path)
348
352
349 cls._static_paths[path] = abspath
353 cls._static_paths[path] = abspath
350 return abspath
354 return abspath
351
355
352 def get(self, path, include_body=True):
356 def get(self, path, include_body=True):
353 path = self.parse_url_path(path)
357 path = self.parse_url_path(path)
354
358
355 # begin subclass override
359 # begin subclass override
356 abspath = self.locate_file(path, self.roots)
360 abspath = self.locate_file(path, self.roots)
357 # end subclass override
361 # end subclass override
358
362
359 if os.path.isdir(abspath) and self.default_filename is not None:
363 if os.path.isdir(abspath) and self.default_filename is not None:
360 # need to look at the request.path here for when path is empty
364 # need to look at the request.path here for when path is empty
361 # but there is some prefix to the path that was already
365 # but there is some prefix to the path that was already
362 # trimmed by the routing
366 # trimmed by the routing
363 if not self.request.path.endswith("/"):
367 if not self.request.path.endswith("/"):
364 self.redirect(self.request.path + "/")
368 self.redirect(self.request.path + "/")
365 return
369 return
366 abspath = os.path.join(abspath, self.default_filename)
370 abspath = os.path.join(abspath, self.default_filename)
367 if not os.path.exists(abspath):
371 if not os.path.exists(abspath):
368 raise HTTPError(404)
372 raise HTTPError(404)
369 if not os.path.isfile(abspath):
373 if not os.path.isfile(abspath):
370 raise HTTPError(403, "%s is not a file", path)
374 raise HTTPError(403, "%s is not a file", path)
371
375
372 stat_result = os.stat(abspath)
376 stat_result = os.stat(abspath)
373 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
377 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
374
378
375 self.set_header("Last-Modified", modified)
379 self.set_header("Last-Modified", modified)
376
380
377 mime_type, encoding = mimetypes.guess_type(abspath)
381 mime_type, encoding = mimetypes.guess_type(abspath)
378 if mime_type:
382 if mime_type:
379 self.set_header("Content-Type", mime_type)
383 self.set_header("Content-Type", mime_type)
380
384
381 cache_time = self.get_cache_time(path, modified, mime_type)
385 cache_time = self.get_cache_time(path, modified, mime_type)
382
386
383 if cache_time > 0:
387 if cache_time > 0:
384 self.set_header("Expires", datetime.datetime.utcnow() + \
388 self.set_header("Expires", datetime.datetime.utcnow() + \
385 datetime.timedelta(seconds=cache_time))
389 datetime.timedelta(seconds=cache_time))
386 self.set_header("Cache-Control", "max-age=" + str(cache_time))
390 self.set_header("Cache-Control", "max-age=" + str(cache_time))
387 else:
391 else:
388 self.set_header("Cache-Control", "public")
392 self.set_header("Cache-Control", "public")
389
393
390 self.set_extra_headers(path)
394 self.set_extra_headers(path)
391
395
392 # Check the If-Modified-Since, and don't send the result if the
396 # Check the If-Modified-Since, and don't send the result if the
393 # content has not been modified
397 # content has not been modified
394 ims_value = self.request.headers.get("If-Modified-Since")
398 ims_value = self.request.headers.get("If-Modified-Since")
395 if ims_value is not None:
399 if ims_value is not None:
396 date_tuple = email.utils.parsedate(ims_value)
400 date_tuple = email.utils.parsedate(ims_value)
397 if_since = datetime.datetime(*date_tuple[:6])
401 if_since = datetime.datetime(*date_tuple[:6])
398 if if_since >= modified:
402 if if_since >= modified:
399 self.set_status(304)
403 self.set_status(304)
400 return
404 return
401
405
402 if os.path.splitext(path)[1] == '.ipynb':
403 name = os.path.basename(path)
404 self.set_header('Content-Type', 'application/json')
405 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
406
407 with open(abspath, "rb") as file:
406 with open(abspath, "rb") as file:
408 data = file.read()
407 data = file.read()
409 hasher = hashlib.sha1()
408 hasher = hashlib.sha1()
410 hasher.update(data)
409 hasher.update(data)
411 self.set_header("Etag", '"%s"' % hasher.hexdigest())
410 self.set_header("Etag", '"%s"' % hasher.hexdigest())
412 if include_body:
411 if include_body:
413 self.write(data)
412 self.write(data)
414 else:
413 else:
415 assert self.request.method == "HEAD"
414 assert self.request.method == "HEAD"
416 self.set_header("Content-Length", len(data))
415 self.set_header("Content-Length", len(data))
417
416
418 @classmethod
417 @classmethod
419 def get_version(cls, settings, path):
418 def get_version(cls, settings, path):
420 """Generate the version string to be used in static URLs.
419 """Generate the version string to be used in static URLs.
421
420
422 This method may be overridden in subclasses (but note that it
421 This method may be overridden in subclasses (but note that it
423 is a class method rather than a static method). The default
422 is a class method rather than a static method). The default
424 implementation uses a hash of the file's contents.
423 implementation uses a hash of the file's contents.
425
424
426 ``settings`` is the `Application.settings` dictionary and ``path``
425 ``settings`` is the `Application.settings` dictionary and ``path``
427 is the relative location of the requested asset on the filesystem.
426 is the relative location of the requested asset on the filesystem.
428 The returned value should be a string, or ``None`` if no version
427 The returned value should be a string, or ``None`` if no version
429 could be determined.
428 could be determined.
430 """
429 """
431 # begin subclass override:
430 # begin subclass override:
432 static_paths = settings['static_path']
431 static_paths = settings['static_path']
433 if isinstance(static_paths, basestring):
432 if isinstance(static_paths, basestring):
434 static_paths = [static_paths]
433 static_paths = [static_paths]
435 roots = tuple(
434 roots = tuple(
436 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
435 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
437 )
436 )
438
437
439 try:
438 try:
440 abs_path = filefind(path, roots)
439 abs_path = filefind(path, roots)
441 except IOError:
440 except IOError:
442 app_log.error("Could not find static file %r", path)
441 app_log.error("Could not find static file %r", path)
443 return None
442 return None
444
443
445 # end subclass override
444 # end subclass override
446
445
447 with cls._lock:
446 with cls._lock:
448 hashes = cls._static_hashes
447 hashes = cls._static_hashes
449 if abs_path not in hashes:
448 if abs_path not in hashes:
450 try:
449 try:
451 f = open(abs_path, "rb")
450 f = open(abs_path, "rb")
452 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
451 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
453 f.close()
452 f.close()
454 except Exception:
453 except Exception:
455 app_log.error("Could not open static file %r", path)
454 app_log.error("Could not open static file %r", path)
456 hashes[abs_path] = None
455 hashes[abs_path] = None
457 hsh = hashes.get(abs_path)
456 hsh = hashes.get(abs_path)
458 if hsh:
457 if hsh:
459 return hsh[:5]
458 return hsh[:5]
460 return None
459 return None
461
460
462
461
463 def parse_url_path(self, url_path):
462 def parse_url_path(self, url_path):
464 """Converts a static URL path into a filesystem path.
463 """Converts a static URL path into a filesystem path.
465
464
466 ``url_path`` is the path component of the URL with
465 ``url_path`` is the path component of the URL with
467 ``static_url_prefix`` removed. The return value should be
466 ``static_url_prefix`` removed. The return value should be
468 filesystem path relative to ``static_path``.
467 filesystem path relative to ``static_path``.
469 """
468 """
470 if os.path.sep != "/":
469 if os.path.sep != "/":
471 url_path = url_path.replace("/", os.path.sep)
470 url_path = url_path.replace("/", os.path.sep)
472 return url_path
471 return url_path
473
472
474 class TrailingSlashHandler(web.RequestHandler):
473 class TrailingSlashHandler(web.RequestHandler):
475 """Simple redirect handler that strips trailing slashes
474 """Simple redirect handler that strips trailing slashes
476
475
477 This should be the first, highest priority handler.
476 This should be the first, highest priority handler.
478 """
477 """
479
478
480 SUPPORTED_METHODS = ['GET']
479 SUPPORTED_METHODS = ['GET']
481
480
482 def get(self):
481 def get(self):
483 self.redirect(self.request.uri.rstrip('/'))
482 self.redirect(self.request.uri.rstrip('/'))
484
483
485 #-----------------------------------------------------------------------------
484 #-----------------------------------------------------------------------------
486 # URL to handler mappings
485 # URL to handler mappings
487 #-----------------------------------------------------------------------------
486 #-----------------------------------------------------------------------------
488
487
489
488
490 default_handlers = [
489 default_handlers = [
491 (r".*/", TrailingSlashHandler)
490 (r".*/", TrailingSlashHandler)
492 ]
491 ]
@@ -1,244 +1,232
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) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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
23 from IPython.html.utils import url_path_join
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
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Notebook web service handlers
29 # Notebook web service handlers
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32
32
33 class NotebookHandler(IPythonHandler):
33 class NotebookHandler(IPythonHandler):
34
34
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36
36
37 def notebook_location(self, name, path=''):
37 def notebook_location(self, name, path=''):
38 """Return the full URL location of a notebook based.
38 """Return the full URL location of a notebook based.
39
39
40 Parameters
40 Parameters
41 ----------
41 ----------
42 name : unicode
42 name : unicode
43 The base name of the notebook, such as "foo.ipynb".
43 The base name of the notebook, such as "foo.ipynb".
44 path : unicode
44 path : unicode
45 The URL path of the notebook.
45 The URL path of the notebook.
46 """
46 """
47 return url_path_join(self.base_project_url, 'api', 'notebooks', path, name)
47 return url_path_join(self.base_project_url, 'api', 'notebooks', path, name)
48
48
49 @web.authenticated
49 @web.authenticated
50 @json_errors
50 @json_errors
51 def get(self, path='', name=None):
51 def get(self, path='', name=None):
52 """Return a Notebook or list of notebooks.
52 """Return a Notebook or list of notebooks.
53
53
54 * GET with path and no notebook name lists notebooks in a directory
54 * GET with path and no notebook name lists notebooks in a directory
55 * GET with path and notebook name returns notebook JSON
55 * GET with path and notebook name returns notebook JSON
56 """
56 """
57 nbm = self.notebook_manager
57 nbm = self.notebook_manager
58 # Check to see if a notebook name was given
58 # Check to see if a notebook name was given
59 if name is None:
59 if name is None:
60 # List notebooks in 'path'
60 # List notebooks in 'path'
61 notebooks = nbm.list_notebooks(path)
61 notebooks = nbm.list_notebooks(path)
62 self.finish(json.dumps(notebooks, default=date_default))
62 self.finish(json.dumps(notebooks, default=date_default))
63 return
63 return
64 # get and return notebook representation
64 # get and return notebook representation
65 model = nbm.get_notebook_model(name, path)
65 model = nbm.get_notebook_model(name, path)
66 self.set_header(u'Last-Modified', model[u'last_modified'])
66 self.set_header(u'Last-Modified', model[u'last_modified'])
67
68 if self.get_argument('download', default='False') == 'True':
69 format = self.get_argument('format', default='json')
70 if format != u'json':
71 self.set_header('Content-Type', 'application/json')
72 raise web.HTTPError(400, "Unrecognized format: %s" % format)
73
74 self.set_header('Content-Disposition',
75 'attachment; filename="%s"' % name
76 )
77 self.finish(json.dumps(model['content'], default=date_default))
78 else:
79 self.finish(json.dumps(model, default=date_default))
67 self.finish(json.dumps(model, default=date_default))
80
68
81 @web.authenticated
69 @web.authenticated
82 @json_errors
70 @json_errors
83 def patch(self, path='', name=None):
71 def patch(self, path='', name=None):
84 """PATCH renames a notebook without re-uploading content."""
72 """PATCH renames a notebook without re-uploading content."""
85 nbm = self.notebook_manager
73 nbm = self.notebook_manager
86 if name is None:
74 if name is None:
87 raise web.HTTPError(400, u'Notebook name missing')
75 raise web.HTTPError(400, u'Notebook name missing')
88 model = self.get_json_body()
76 model = self.get_json_body()
89 if model is None:
77 if model is None:
90 raise web.HTTPError(400, u'JSON body missing')
78 raise web.HTTPError(400, u'JSON body missing')
91 model = nbm.update_notebook_model(model, name, path)
79 model = nbm.update_notebook_model(model, name, path)
92 location = self.notebook_location(model[u'name'], model[u'path'])
80 location = self.notebook_location(model[u'name'], model[u'path'])
93 self.set_header(u'Location', location)
81 self.set_header(u'Location', location)
94 self.set_header(u'Last-Modified', model[u'last_modified'])
82 self.set_header(u'Last-Modified', model[u'last_modified'])
95 self.finish(json.dumps(model, default=date_default))
83 self.finish(json.dumps(model, default=date_default))
96
84
97 @web.authenticated
85 @web.authenticated
98 @json_errors
86 @json_errors
99 def post(self, path='', name=None):
87 def post(self, path='', name=None):
100 """Create a new notebook in the specified path.
88 """Create a new notebook in the specified path.
101
89
102 POST creates new notebooks.
90 POST creates new notebooks.
103
91
104 POST /api/notebooks/path : new untitled notebook in path
92 POST /api/notebooks/path : new untitled notebook in path
105 POST /api/notebooks/path/notebook.ipynb : new notebook with name in path
93 POST /api/notebooks/path/notebook.ipynb : new notebook with name in path
106 If content specified upload notebook, otherwise start empty.
94 If content specified upload notebook, otherwise start empty.
107 """
95 """
108 nbm = self.notebook_manager
96 nbm = self.notebook_manager
109 model = self.get_json_body()
97 model = self.get_json_body()
110 if name is None:
98 if name is None:
111 # creating new notebook, model doesn't make sense
99 # creating new notebook, model doesn't make sense
112 if model is not None:
100 if model is not None:
113 raise web.HTTPError(400, "Model not valid when creating untitled notebooks.")
101 raise web.HTTPError(400, "Model not valid when creating untitled notebooks.")
114 model = nbm.create_notebook_model(path=path)
102 model = nbm.create_notebook_model(path=path)
115 else:
103 else:
116 if model is None:
104 if model is None:
117 self.log.info("Creating new Notebook at %s/%s", path, name)
105 self.log.info("Creating new Notebook at %s/%s", path, name)
118 model = {}
106 model = {}
119 else:
107 else:
120 self.log.info("Uploading Notebook to %s/%s", path, name)
108 self.log.info("Uploading Notebook to %s/%s", path, name)
121 # set the model name from the URL
109 # set the model name from the URL
122 model['name'] = name
110 model['name'] = name
123 model = nbm.create_notebook_model(model, path)
111 model = nbm.create_notebook_model(model, path)
124
112
125 location = self.notebook_location(model[u'name'], model[u'path'])
113 location = self.notebook_location(model[u'name'], model[u'path'])
126 self.set_header(u'Location', location)
114 self.set_header(u'Location', location)
127 self.set_header(u'Last-Modified', model[u'last_modified'])
115 self.set_header(u'Last-Modified', model[u'last_modified'])
128 self.set_status(201)
116 self.set_status(201)
129 self.finish(json.dumps(model, default=date_default))
117 self.finish(json.dumps(model, default=date_default))
130
118
131 @web.authenticated
119 @web.authenticated
132 @json_errors
120 @json_errors
133 def put(self, path='', name=None):
121 def put(self, path='', name=None):
134 """saves the notebook in the location given by 'notebook_path'."""
122 """saves the notebook in the location given by 'notebook_path'."""
135 nbm = self.notebook_manager
123 nbm = self.notebook_manager
136 model = self.get_json_body()
124 model = self.get_json_body()
137 if model is None:
125 if model is None:
138 raise web.HTTPError(400, u'JSON body missing')
126 raise web.HTTPError(400, u'JSON body missing')
139 nbm.save_notebook_model(model, name, path)
127 nbm.save_notebook_model(model, name, path)
140 self.finish(json.dumps(model, default=date_default))
128 self.finish(json.dumps(model, default=date_default))
141
129
142 @web.authenticated
130 @web.authenticated
143 @json_errors
131 @json_errors
144 def delete(self, path='', name=None):
132 def delete(self, path='', name=None):
145 """delete the notebook in the given notebook path"""
133 """delete the notebook in the given notebook path"""
146 nbm = self.notebook_manager
134 nbm = self.notebook_manager
147 nbm.delete_notebook_model(name, path)
135 nbm.delete_notebook_model(name, path)
148 self.set_status(204)
136 self.set_status(204)
149 self.finish()
137 self.finish()
150
138
151 class NotebookCopyHandler(IPythonHandler):
139 class NotebookCopyHandler(IPythonHandler):
152
140
153 SUPPORTED_METHODS = ('POST')
141 SUPPORTED_METHODS = ('POST')
154
142
155 @web.authenticated
143 @web.authenticated
156 @json_errors
144 @json_errors
157 def post(self, path='', name=None):
145 def post(self, path='', name=None):
158 """Copy an existing notebook."""
146 """Copy an existing notebook."""
159 nbm = self.notebook_manager
147 nbm = self.notebook_manager
160 model = self.get_json_body()
148 model = self.get_json_body()
161 if name is None:
149 if name is None:
162 raise web.HTTPError(400, "Notebook name required")
150 raise web.HTTPError(400, "Notebook name required")
163 self.log.info("Copying Notebook %s/%s", path, name)
151 self.log.info("Copying Notebook %s/%s", path, name)
164 model = nbm.copy_notebook(name, path)
152 model = nbm.copy_notebook(name, path)
165 location = url_path_join(
153 location = url_path_join(
166 self.base_project_url, 'api', 'notebooks',
154 self.base_project_url, 'api', 'notebooks',
167 model['path'], model['name'],
155 model['path'], model['name'],
168 )
156 )
169 self.set_header(u'Location', location)
157 self.set_header(u'Location', location)
170 self.set_header(u'Last-Modified', model[u'last_modified'])
158 self.set_header(u'Last-Modified', model[u'last_modified'])
171 self.set_status(201)
159 self.set_status(201)
172 self.finish(json.dumps(model, default=date_default))
160 self.finish(json.dumps(model, default=date_default))
173
161
174
162
175 class NotebookCheckpointsHandler(IPythonHandler):
163 class NotebookCheckpointsHandler(IPythonHandler):
176
164
177 SUPPORTED_METHODS = ('GET', 'POST')
165 SUPPORTED_METHODS = ('GET', 'POST')
178
166
179 @web.authenticated
167 @web.authenticated
180 @json_errors
168 @json_errors
181 def get(self, path='', name=None):
169 def get(self, path='', name=None):
182 """get lists checkpoints for a notebook"""
170 """get lists checkpoints for a notebook"""
183 nbm = self.notebook_manager
171 nbm = self.notebook_manager
184 checkpoints = nbm.list_checkpoints(name, path)
172 checkpoints = nbm.list_checkpoints(name, path)
185 data = json.dumps(checkpoints, default=date_default)
173 data = json.dumps(checkpoints, default=date_default)
186 self.finish(data)
174 self.finish(data)
187
175
188 @web.authenticated
176 @web.authenticated
189 @json_errors
177 @json_errors
190 def post(self, path='', name=None):
178 def post(self, path='', name=None):
191 """post creates a new checkpoint"""
179 """post creates a new checkpoint"""
192 nbm = self.notebook_manager
180 nbm = self.notebook_manager
193 checkpoint = nbm.create_checkpoint(name, path)
181 checkpoint = nbm.create_checkpoint(name, path)
194 data = json.dumps(checkpoint, default=date_default)
182 data = json.dumps(checkpoint, default=date_default)
195 location = url_path_join(self.base_project_url, u'/api/notebooks',
183 location = url_path_join(self.base_project_url, u'/api/notebooks',
196 path, name, 'checkpoints', checkpoint[u'checkpoint_id'])
184 path, name, 'checkpoints', checkpoint[u'checkpoint_id'])
197 self.set_header(u'Location', location)
185 self.set_header(u'Location', location)
198 self.set_status(201)
186 self.set_status(201)
199 self.finish(data)
187 self.finish(data)
200
188
201
189
202 class ModifyNotebookCheckpointsHandler(IPythonHandler):
190 class ModifyNotebookCheckpointsHandler(IPythonHandler):
203
191
204 SUPPORTED_METHODS = ('POST', 'DELETE')
192 SUPPORTED_METHODS = ('POST', 'DELETE')
205
193
206 @web.authenticated
194 @web.authenticated
207 @json_errors
195 @json_errors
208 def post(self, path, name, checkpoint_id):
196 def post(self, path, name, checkpoint_id):
209 """post restores a notebook from a checkpoint"""
197 """post restores a notebook from a checkpoint"""
210 nbm = self.notebook_manager
198 nbm = self.notebook_manager
211 nbm.restore_checkpoint(checkpoint_id, name, path)
199 nbm.restore_checkpoint(checkpoint_id, name, path)
212 self.set_status(204)
200 self.set_status(204)
213 self.finish()
201 self.finish()
214
202
215 @web.authenticated
203 @web.authenticated
216 @json_errors
204 @json_errors
217 def delete(self, path, name, checkpoint_id):
205 def delete(self, path, name, checkpoint_id):
218 """delete clears a checkpoint for a given notebook"""
206 """delete clears a checkpoint for a given notebook"""
219 nbm = self.notebook_manager
207 nbm = self.notebook_manager
220 nbm.delete_checkpoint(checkpoint_id, name, path)
208 nbm.delete_checkpoint(checkpoint_id, name, path)
221 self.set_status(204)
209 self.set_status(204)
222 self.finish()
210 self.finish()
223
211
224 #-----------------------------------------------------------------------------
212 #-----------------------------------------------------------------------------
225 # URL to handler mappings
213 # URL to handler mappings
226 #-----------------------------------------------------------------------------
214 #-----------------------------------------------------------------------------
227
215
228
216
229 _path_regex = r"(?P<path>(?:/.*)*)"
217 _path_regex = r"(?P<path>(?:/.*)*)"
230 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
218 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
231 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
219 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
232 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
220 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
233
221
234 default_handlers = [
222 default_handlers = [
235 (r"/api/notebooks%s/copy" % _notebook_path_regex, NotebookCopyHandler),
223 (r"/api/notebooks%s/copy" % _notebook_path_regex, NotebookCopyHandler),
236 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
224 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
237 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
225 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
238 ModifyNotebookCheckpointsHandler),
226 ModifyNotebookCheckpointsHandler),
239 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
227 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
240 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
228 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
241 ]
229 ]
242
230
243
231
244
232
@@ -1,305 +1,305
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // MenuBar
9 // MenuBar
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule MenuBar
15 * @submodule MenuBar
16 */
16 */
17
17
18
18
19 var IPython = (function (IPython) {
19 var IPython = (function (IPython) {
20 "use strict";
20 "use strict";
21
21
22 var utils = IPython.utils;
22 var utils = IPython.utils;
23
23
24 /**
24 /**
25 * A MenuBar Class to generate the menubar of IPython notebook
25 * A MenuBar Class to generate the menubar of IPython notebook
26 * @Class MenuBar
26 * @Class MenuBar
27 *
27 *
28 * @constructor
28 * @constructor
29 *
29 *
30 *
30 *
31 * @param selector {string} selector for the menubar element in DOM
31 * @param selector {string} selector for the menubar element in DOM
32 * @param {object} [options]
32 * @param {object} [options]
33 * @param [options.baseProjectUrl] {String} String to use for the
33 * @param [options.baseProjectUrl] {String} String to use for the
34 * Base Project url, default would be to inspect
34 * Base Project url, default would be to inspect
35 * $('body').data('baseProjectUrl');
35 * $('body').data('baseProjectUrl');
36 * does not support change for now is set through this option
36 * does not support change for now is set through this option
37 */
37 */
38 var MenuBar = function (selector, options) {
38 var MenuBar = function (selector, options) {
39 options = options || {};
39 options = options || {};
40 if (options.baseProjectUrl !== undefined) {
40 if (options.baseProjectUrl !== undefined) {
41 this._baseProjectUrl = options.baseProjectUrl;
41 this._baseProjectUrl = options.baseProjectUrl;
42 }
42 }
43 this.selector = selector;
43 this.selector = selector;
44 if (this.selector !== undefined) {
44 if (this.selector !== undefined) {
45 this.element = $(selector);
45 this.element = $(selector);
46 this.style();
46 this.style();
47 this.bind_events();
47 this.bind_events();
48 }
48 }
49 };
49 };
50
50
51 MenuBar.prototype.baseProjectUrl = function(){
51 MenuBar.prototype.baseProjectUrl = function(){
52 return this._baseProjectUrl || $('body').data('baseProjectUrl');
52 return this._baseProjectUrl || $('body').data('baseProjectUrl');
53 };
53 };
54
54
55 MenuBar.prototype.notebookPath = function() {
55 MenuBar.prototype.notebookPath = function() {
56 var path = $('body').data('notebookPath');
56 var path = $('body').data('notebookPath');
57 path = decodeURIComponent(path);
57 path = decodeURIComponent(path);
58 return path;
58 return path;
59 };
59 };
60
60
61 MenuBar.prototype.style = function () {
61 MenuBar.prototype.style = function () {
62 this.element.addClass('border-box-sizing');
62 this.element.addClass('border-box-sizing');
63 this.element.find("li").click(function (event, ui) {
63 this.element.find("li").click(function (event, ui) {
64 // The selected cell loses focus when the menu is entered, so we
64 // The selected cell loses focus when the menu is entered, so we
65 // re-select it upon selection.
65 // re-select it upon selection.
66 var i = IPython.notebook.get_selected_index();
66 var i = IPython.notebook.get_selected_index();
67 IPython.notebook.select(i);
67 IPython.notebook.select(i);
68 }
68 }
69 );
69 );
70 };
70 };
71
71
72
72
73 MenuBar.prototype.bind_events = function () {
73 MenuBar.prototype.bind_events = function () {
74 // File
74 // File
75 var that = this;
75 var that = this;
76 this.element.find('#new_notebook').click(function () {
76 this.element.find('#new_notebook').click(function () {
77 IPython.notebook.new_notebook();
77 IPython.notebook.new_notebook();
78 });
78 });
79 this.element.find('#open_notebook').click(function () {
79 this.element.find('#open_notebook').click(function () {
80 window.open(utils.url_path_join(
80 window.open(utils.url_path_join(
81 that.baseProjectUrl(),
81 that.baseProjectUrl(),
82 'tree',
82 'tree',
83 that.notebookPath()
83 that.notebookPath()
84 ));
84 ));
85 });
85 });
86 this.element.find('#copy_notebook').click(function () {
86 this.element.find('#copy_notebook').click(function () {
87 IPython.notebook.copy_notebook();
87 IPython.notebook.copy_notebook();
88 return false;
88 return false;
89 });
89 });
90 this.element.find('#download_ipynb').click(function () {
90 this.element.find('#download_ipynb').click(function () {
91 var notebook_name = IPython.notebook.get_notebook_name();
91 var notebook_name = IPython.notebook.get_notebook_name();
92 if (IPython.notebook.dirty) {
92 if (IPython.notebook.dirty) {
93 IPython.notebook.save_notebook({async : false});
93 IPython.notebook.save_notebook({async : false});
94 }
94 }
95
95
96 var url = utils.url_path_join(
96 var url = utils.url_path_join(
97 that.baseProjectUrl(),
97 that.baseProjectUrl(),
98 'api/notebooks',
98 'files',
99 that.notebookPath(),
99 that.notebookPath(),
100 notebook_name + '.ipynb?format=json&download=True'
100 notebook_name + '.ipynb'
101 );
101 );
102 window.location.assign(url);
102 window.location.assign(url);
103 });
103 });
104
104
105 /* FIXME: download-as-py doesn't work right now
105 /* FIXME: download-as-py doesn't work right now
106 * We will need nbconvert hooked up to get this back
106 * We will need nbconvert hooked up to get this back
107
107
108 this.element.find('#download_py').click(function () {
108 this.element.find('#download_py').click(function () {
109 var notebook_name = IPython.notebook.get_notebook_name();
109 var notebook_name = IPython.notebook.get_notebook_name();
110 if (IPython.notebook.dirty) {
110 if (IPython.notebook.dirty) {
111 IPython.notebook.save_notebook({async : false});
111 IPython.notebook.save_notebook({async : false});
112 }
112 }
113 var url = utils.url_path_join(
113 var url = utils.url_path_join(
114 that.baseProjectUrl(),
114 that.baseProjectUrl(),
115 'api/notebooks',
115 'api/notebooks',
116 that.notebookPath(),
116 that.notebookPath(),
117 notebook_name + '.ipynb?format=py&download=True'
117 notebook_name + '.ipynb?format=py&download=True'
118 );
118 );
119 window.location.assign(url);
119 window.location.assign(url);
120 });
120 });
121
121
122 */
122 */
123
123
124 this.element.find('#rename_notebook').click(function () {
124 this.element.find('#rename_notebook').click(function () {
125 IPython.save_widget.rename_notebook();
125 IPython.save_widget.rename_notebook();
126 });
126 });
127 this.element.find('#save_checkpoint').click(function () {
127 this.element.find('#save_checkpoint').click(function () {
128 IPython.notebook.save_checkpoint();
128 IPython.notebook.save_checkpoint();
129 });
129 });
130 this.element.find('#restore_checkpoint').click(function () {
130 this.element.find('#restore_checkpoint').click(function () {
131 });
131 });
132 this.element.find('#kill_and_exit').click(function () {
132 this.element.find('#kill_and_exit').click(function () {
133 IPython.notebook.session.delete_session();
133 IPython.notebook.session.delete_session();
134 setTimeout(function(){window.close();}, 500);
134 setTimeout(function(){window.close();}, 500);
135 });
135 });
136 // Edit
136 // Edit
137 this.element.find('#cut_cell').click(function () {
137 this.element.find('#cut_cell').click(function () {
138 IPython.notebook.cut_cell();
138 IPython.notebook.cut_cell();
139 });
139 });
140 this.element.find('#copy_cell').click(function () {
140 this.element.find('#copy_cell').click(function () {
141 IPython.notebook.copy_cell();
141 IPython.notebook.copy_cell();
142 });
142 });
143 this.element.find('#delete_cell').click(function () {
143 this.element.find('#delete_cell').click(function () {
144 IPython.notebook.delete_cell();
144 IPython.notebook.delete_cell();
145 });
145 });
146 this.element.find('#undelete_cell').click(function () {
146 this.element.find('#undelete_cell').click(function () {
147 IPython.notebook.undelete();
147 IPython.notebook.undelete();
148 });
148 });
149 this.element.find('#split_cell').click(function () {
149 this.element.find('#split_cell').click(function () {
150 IPython.notebook.split_cell();
150 IPython.notebook.split_cell();
151 });
151 });
152 this.element.find('#merge_cell_above').click(function () {
152 this.element.find('#merge_cell_above').click(function () {
153 IPython.notebook.merge_cell_above();
153 IPython.notebook.merge_cell_above();
154 });
154 });
155 this.element.find('#merge_cell_below').click(function () {
155 this.element.find('#merge_cell_below').click(function () {
156 IPython.notebook.merge_cell_below();
156 IPython.notebook.merge_cell_below();
157 });
157 });
158 this.element.find('#move_cell_up').click(function () {
158 this.element.find('#move_cell_up').click(function () {
159 IPython.notebook.move_cell_up();
159 IPython.notebook.move_cell_up();
160 });
160 });
161 this.element.find('#move_cell_down').click(function () {
161 this.element.find('#move_cell_down').click(function () {
162 IPython.notebook.move_cell_down();
162 IPython.notebook.move_cell_down();
163 });
163 });
164 this.element.find('#select_previous').click(function () {
164 this.element.find('#select_previous').click(function () {
165 IPython.notebook.select_prev();
165 IPython.notebook.select_prev();
166 });
166 });
167 this.element.find('#select_next').click(function () {
167 this.element.find('#select_next').click(function () {
168 IPython.notebook.select_next();
168 IPython.notebook.select_next();
169 });
169 });
170 this.element.find('#edit_nb_metadata').click(function () {
170 this.element.find('#edit_nb_metadata').click(function () {
171 IPython.notebook.edit_metadata();
171 IPython.notebook.edit_metadata();
172 });
172 });
173
173
174 // View
174 // View
175 this.element.find('#toggle_header').click(function () {
175 this.element.find('#toggle_header').click(function () {
176 $('div#header').toggle();
176 $('div#header').toggle();
177 IPython.layout_manager.do_resize();
177 IPython.layout_manager.do_resize();
178 });
178 });
179 this.element.find('#toggle_toolbar').click(function () {
179 this.element.find('#toggle_toolbar').click(function () {
180 $('div#maintoolbar').toggle();
180 $('div#maintoolbar').toggle();
181 IPython.layout_manager.do_resize();
181 IPython.layout_manager.do_resize();
182 });
182 });
183 // Insert
183 // Insert
184 this.element.find('#insert_cell_above').click(function () {
184 this.element.find('#insert_cell_above').click(function () {
185 IPython.notebook.insert_cell_above('code');
185 IPython.notebook.insert_cell_above('code');
186 });
186 });
187 this.element.find('#insert_cell_below').click(function () {
187 this.element.find('#insert_cell_below').click(function () {
188 IPython.notebook.insert_cell_below('code');
188 IPython.notebook.insert_cell_below('code');
189 });
189 });
190 // Cell
190 // Cell
191 this.element.find('#run_cell').click(function () {
191 this.element.find('#run_cell').click(function () {
192 IPython.notebook.execute_selected_cell();
192 IPython.notebook.execute_selected_cell();
193 });
193 });
194 this.element.find('#run_cell_in_place').click(function () {
194 this.element.find('#run_cell_in_place').click(function () {
195 IPython.notebook.execute_selected_cell({terminal:true});
195 IPython.notebook.execute_selected_cell({terminal:true});
196 });
196 });
197 this.element.find('#run_all_cells').click(function () {
197 this.element.find('#run_all_cells').click(function () {
198 IPython.notebook.execute_all_cells();
198 IPython.notebook.execute_all_cells();
199 }).attr('title', 'Run all cells in the notebook');
199 }).attr('title', 'Run all cells in the notebook');
200 this.element.find('#run_all_cells_above').click(function () {
200 this.element.find('#run_all_cells_above').click(function () {
201 IPython.notebook.execute_cells_above();
201 IPython.notebook.execute_cells_above();
202 }).attr('title', 'Run all cells above (but not including) this cell');
202 }).attr('title', 'Run all cells above (but not including) this cell');
203 this.element.find('#run_all_cells_below').click(function () {
203 this.element.find('#run_all_cells_below').click(function () {
204 IPython.notebook.execute_cells_below();
204 IPython.notebook.execute_cells_below();
205 }).attr('title', 'Run this cell and all cells below it');
205 }).attr('title', 'Run this cell and all cells below it');
206 this.element.find('#to_code').click(function () {
206 this.element.find('#to_code').click(function () {
207 IPython.notebook.to_code();
207 IPython.notebook.to_code();
208 });
208 });
209 this.element.find('#to_markdown').click(function () {
209 this.element.find('#to_markdown').click(function () {
210 IPython.notebook.to_markdown();
210 IPython.notebook.to_markdown();
211 });
211 });
212 this.element.find('#to_raw').click(function () {
212 this.element.find('#to_raw').click(function () {
213 IPython.notebook.to_raw();
213 IPython.notebook.to_raw();
214 });
214 });
215 this.element.find('#to_heading1').click(function () {
215 this.element.find('#to_heading1').click(function () {
216 IPython.notebook.to_heading(undefined, 1);
216 IPython.notebook.to_heading(undefined, 1);
217 });
217 });
218 this.element.find('#to_heading2').click(function () {
218 this.element.find('#to_heading2').click(function () {
219 IPython.notebook.to_heading(undefined, 2);
219 IPython.notebook.to_heading(undefined, 2);
220 });
220 });
221 this.element.find('#to_heading3').click(function () {
221 this.element.find('#to_heading3').click(function () {
222 IPython.notebook.to_heading(undefined, 3);
222 IPython.notebook.to_heading(undefined, 3);
223 });
223 });
224 this.element.find('#to_heading4').click(function () {
224 this.element.find('#to_heading4').click(function () {
225 IPython.notebook.to_heading(undefined, 4);
225 IPython.notebook.to_heading(undefined, 4);
226 });
226 });
227 this.element.find('#to_heading5').click(function () {
227 this.element.find('#to_heading5').click(function () {
228 IPython.notebook.to_heading(undefined, 5);
228 IPython.notebook.to_heading(undefined, 5);
229 });
229 });
230 this.element.find('#to_heading6').click(function () {
230 this.element.find('#to_heading6').click(function () {
231 IPython.notebook.to_heading(undefined, 6);
231 IPython.notebook.to_heading(undefined, 6);
232 });
232 });
233 this.element.find('#toggle_output').click(function () {
233 this.element.find('#toggle_output').click(function () {
234 IPython.notebook.toggle_output();
234 IPython.notebook.toggle_output();
235 });
235 });
236 this.element.find('#collapse_all_output').click(function () {
236 this.element.find('#collapse_all_output').click(function () {
237 IPython.notebook.collapse_all_output();
237 IPython.notebook.collapse_all_output();
238 });
238 });
239 this.element.find('#scroll_all_output').click(function () {
239 this.element.find('#scroll_all_output').click(function () {
240 IPython.notebook.scroll_all_output();
240 IPython.notebook.scroll_all_output();
241 });
241 });
242 this.element.find('#expand_all_output').click(function () {
242 this.element.find('#expand_all_output').click(function () {
243 IPython.notebook.expand_all_output();
243 IPython.notebook.expand_all_output();
244 });
244 });
245 this.element.find('#clear_all_output').click(function () {
245 this.element.find('#clear_all_output').click(function () {
246 IPython.notebook.clear_all_output();
246 IPython.notebook.clear_all_output();
247 });
247 });
248 // Kernel
248 // Kernel
249 this.element.find('#int_kernel').click(function () {
249 this.element.find('#int_kernel').click(function () {
250 IPython.notebook.session.interrupt_kernel();
250 IPython.notebook.session.interrupt_kernel();
251 });
251 });
252 this.element.find('#restart_kernel').click(function () {
252 this.element.find('#restart_kernel').click(function () {
253 IPython.notebook.restart_kernel();
253 IPython.notebook.restart_kernel();
254 });
254 });
255 // Help
255 // Help
256 this.element.find('#keyboard_shortcuts').click(function () {
256 this.element.find('#keyboard_shortcuts').click(function () {
257 IPython.quick_help.show_keyboard_shortcuts();
257 IPython.quick_help.show_keyboard_shortcuts();
258 });
258 });
259
259
260 this.update_restore_checkpoint(null);
260 this.update_restore_checkpoint(null);
261
261
262 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
262 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
263 that.update_restore_checkpoint(IPython.notebook.checkpoints);
263 that.update_restore_checkpoint(IPython.notebook.checkpoints);
264 });
264 });
265
265
266 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
266 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
267 that.update_restore_checkpoint(IPython.notebook.checkpoints);
267 that.update_restore_checkpoint(IPython.notebook.checkpoints);
268 });
268 });
269 };
269 };
270
270
271 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
271 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
272 var ul = this.element.find("#restore_checkpoint").find("ul");
272 var ul = this.element.find("#restore_checkpoint").find("ul");
273 ul.empty();
273 ul.empty();
274 if (!checkpoints || checkpoints.length === 0) {
274 if (!checkpoints || checkpoints.length === 0) {
275 ul.append(
275 ul.append(
276 $("<li/>")
276 $("<li/>")
277 .addClass("disabled")
277 .addClass("disabled")
278 .append(
278 .append(
279 $("<a/>")
279 $("<a/>")
280 .text("No checkpoints")
280 .text("No checkpoints")
281 )
281 )
282 );
282 );
283 return;
283 return;
284 }
284 }
285
285
286 checkpoints.map(function (checkpoint) {
286 checkpoints.map(function (checkpoint) {
287 var d = new Date(checkpoint.last_modified);
287 var d = new Date(checkpoint.last_modified);
288 ul.append(
288 ul.append(
289 $("<li/>").append(
289 $("<li/>").append(
290 $("<a/>")
290 $("<a/>")
291 .attr("href", "#")
291 .attr("href", "#")
292 .text(d.format("mmm dd HH:MM:ss"))
292 .text(d.format("mmm dd HH:MM:ss"))
293 .click(function () {
293 .click(function () {
294 IPython.notebook.restore_checkpoint_dialog(checkpoint);
294 IPython.notebook.restore_checkpoint_dialog(checkpoint);
295 })
295 })
296 )
296 )
297 );
297 );
298 });
298 });
299 };
299 };
300
300
301 IPython.MenuBar = MenuBar;
301 IPython.MenuBar = MenuBar;
302
302
303 return IPython;
303 return IPython;
304
304
305 }(IPython));
305 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now