##// END OF EJS Templates
rename notebooks service to contents service...
MinRK -
Show More
@@ -1,427 +1,427 b''
1 """Base Tornado handlers for the notebook."""
1 """Base Tornado handlers for the notebook server."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 try:
22 try:
23 from tornado.log import app_log
23 from tornado.log import app_log
24 except ImportError:
24 except ImportError:
25 app_log = logging.getLogger()
25 app_log = logging.getLogger()
26
26
27 from IPython.config import Application
27 from IPython.config import Application
28 from IPython.utils.path import filefind
28 from IPython.utils.path import filefind
29 from IPython.utils.py3compat import string_types
29 from IPython.utils.py3compat import string_types
30 from IPython.html.utils import is_hidden
30 from IPython.html.utils import is_hidden
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Top-level handlers
33 # Top-level handlers
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36
36
37 class AuthenticatedHandler(web.RequestHandler):
37 class AuthenticatedHandler(web.RequestHandler):
38 """A RequestHandler with an authenticated user."""
38 """A RequestHandler with an authenticated user."""
39
39
40 def set_default_headers(self):
40 def set_default_headers(self):
41 headers = self.settings.get('headers', {})
41 headers = self.settings.get('headers', {})
42
42
43 if "X-Frame-Options" not in headers:
43 if "X-Frame-Options" not in headers:
44 headers["X-Frame-Options"] = "SAMEORIGIN"
44 headers["X-Frame-Options"] = "SAMEORIGIN"
45
45
46 for header_name,value in headers.items() :
46 for header_name,value in headers.items() :
47 try:
47 try:
48 self.set_header(header_name, value)
48 self.set_header(header_name, value)
49 except Exception:
49 except Exception:
50 # tornado raise Exception (not a subclass)
50 # tornado raise Exception (not a subclass)
51 # if method is unsupported (websocket and Access-Control-Allow-Origin
51 # if method is unsupported (websocket and Access-Control-Allow-Origin
52 # for example, so just ignore)
52 # for example, so just ignore)
53 pass
53 pass
54
54
55 def clear_login_cookie(self):
55 def clear_login_cookie(self):
56 self.clear_cookie(self.cookie_name)
56 self.clear_cookie(self.cookie_name)
57
57
58 def get_current_user(self):
58 def get_current_user(self):
59 user_id = self.get_secure_cookie(self.cookie_name)
59 user_id = self.get_secure_cookie(self.cookie_name)
60 # For now the user_id should not return empty, but it could eventually
60 # For now the user_id should not return empty, but it could eventually
61 if user_id == '':
61 if user_id == '':
62 user_id = 'anonymous'
62 user_id = 'anonymous'
63 if user_id is None:
63 if user_id is None:
64 # prevent extra Invalid cookie sig warnings:
64 # prevent extra Invalid cookie sig warnings:
65 self.clear_login_cookie()
65 self.clear_login_cookie()
66 if not self.login_available:
66 if not self.login_available:
67 user_id = 'anonymous'
67 user_id = 'anonymous'
68 return user_id
68 return user_id
69
69
70 @property
70 @property
71 def cookie_name(self):
71 def cookie_name(self):
72 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
72 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
73 self.request.host
73 self.request.host
74 ))
74 ))
75 return self.settings.get('cookie_name', default_cookie_name)
75 return self.settings.get('cookie_name', default_cookie_name)
76
76
77 @property
77 @property
78 def password(self):
78 def password(self):
79 """our password"""
79 """our password"""
80 return self.settings.get('password', '')
80 return self.settings.get('password', '')
81
81
82 @property
82 @property
83 def logged_in(self):
83 def logged_in(self):
84 """Is a user currently logged in?
84 """Is a user currently logged in?
85
85
86 """
86 """
87 user = self.get_current_user()
87 user = self.get_current_user()
88 return (user and not user == 'anonymous')
88 return (user and not user == 'anonymous')
89
89
90 @property
90 @property
91 def login_available(self):
91 def login_available(self):
92 """May a user proceed to log in?
92 """May a user proceed to log in?
93
93
94 This returns True if login capability is available, irrespective of
94 This returns True if login capability is available, irrespective of
95 whether the user is already logged in or not.
95 whether the user is already logged in or not.
96
96
97 """
97 """
98 return bool(self.settings.get('password', ''))
98 return bool(self.settings.get('password', ''))
99
99
100
100
101 class IPythonHandler(AuthenticatedHandler):
101 class IPythonHandler(AuthenticatedHandler):
102 """IPython-specific extensions to authenticated handling
102 """IPython-specific extensions to authenticated handling
103
103
104 Mostly property shortcuts to IPython-specific settings.
104 Mostly property shortcuts to IPython-specific settings.
105 """
105 """
106
106
107 @property
107 @property
108 def config(self):
108 def config(self):
109 return self.settings.get('config', None)
109 return self.settings.get('config', None)
110
110
111 @property
111 @property
112 def log(self):
112 def log(self):
113 """use the IPython log by default, falling back on tornado's logger"""
113 """use the IPython log by default, falling back on tornado's logger"""
114 if Application.initialized():
114 if Application.initialized():
115 return Application.instance().log
115 return Application.instance().log
116 else:
116 else:
117 return app_log
117 return app_log
118
118
119 #---------------------------------------------------------------
119 #---------------------------------------------------------------
120 # URLs
120 # URLs
121 #---------------------------------------------------------------
121 #---------------------------------------------------------------
122
122
123 @property
123 @property
124 def mathjax_url(self):
124 def mathjax_url(self):
125 return self.settings.get('mathjax_url', '')
125 return self.settings.get('mathjax_url', '')
126
126
127 @property
127 @property
128 def base_url(self):
128 def base_url(self):
129 return self.settings.get('base_url', '/')
129 return self.settings.get('base_url', '/')
130
130
131 @property
131 @property
132 def ws_url(self):
132 def ws_url(self):
133 return self.settings.get('websocket_url', '')
133 return self.settings.get('websocket_url', '')
134
134
135 #---------------------------------------------------------------
135 #---------------------------------------------------------------
136 # Manager objects
136 # Manager objects
137 #---------------------------------------------------------------
137 #---------------------------------------------------------------
138
138
139 @property
139 @property
140 def kernel_manager(self):
140 def kernel_manager(self):
141 return self.settings['kernel_manager']
141 return self.settings['kernel_manager']
142
142
143 @property
143 @property
144 def notebook_manager(self):
144 def contents_manager(self):
145 return self.settings['notebook_manager']
145 return self.settings['contents_manager']
146
146
147 @property
147 @property
148 def cluster_manager(self):
148 def cluster_manager(self):
149 return self.settings['cluster_manager']
149 return self.settings['cluster_manager']
150
150
151 @property
151 @property
152 def session_manager(self):
152 def session_manager(self):
153 return self.settings['session_manager']
153 return self.settings['session_manager']
154
154
155 @property
155 @property
156 def kernel_spec_manager(self):
156 def kernel_spec_manager(self):
157 return self.settings['kernel_spec_manager']
157 return self.settings['kernel_spec_manager']
158
158
159 @property
159 @property
160 def project_dir(self):
160 def project_dir(self):
161 return self.notebook_manager.notebook_dir
161 return getattr(self.contents_manager, 'root_dir', '/')
162
162
163 #---------------------------------------------------------------
163 #---------------------------------------------------------------
164 # CORS
164 # CORS
165 #---------------------------------------------------------------
165 #---------------------------------------------------------------
166
166
167 @property
167 @property
168 def allow_origin(self):
168 def allow_origin(self):
169 """Normal Access-Control-Allow-Origin"""
169 """Normal Access-Control-Allow-Origin"""
170 return self.settings.get('allow_origin', '')
170 return self.settings.get('allow_origin', '')
171
171
172 @property
172 @property
173 def allow_origin_pat(self):
173 def allow_origin_pat(self):
174 """Regular expression version of allow_origin"""
174 """Regular expression version of allow_origin"""
175 return self.settings.get('allow_origin_pat', None)
175 return self.settings.get('allow_origin_pat', None)
176
176
177 @property
177 @property
178 def allow_credentials(self):
178 def allow_credentials(self):
179 """Whether to set Access-Control-Allow-Credentials"""
179 """Whether to set Access-Control-Allow-Credentials"""
180 return self.settings.get('allow_credentials', False)
180 return self.settings.get('allow_credentials', False)
181
181
182 def set_default_headers(self):
182 def set_default_headers(self):
183 """Add CORS headers, if defined"""
183 """Add CORS headers, if defined"""
184 super(IPythonHandler, self).set_default_headers()
184 super(IPythonHandler, self).set_default_headers()
185 if self.allow_origin:
185 if self.allow_origin:
186 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
186 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
187 elif self.allow_origin_pat:
187 elif self.allow_origin_pat:
188 origin = self.get_origin()
188 origin = self.get_origin()
189 if origin and self.allow_origin_pat.match(origin):
189 if origin and self.allow_origin_pat.match(origin):
190 self.set_header("Access-Control-Allow-Origin", origin)
190 self.set_header("Access-Control-Allow-Origin", origin)
191 if self.allow_credentials:
191 if self.allow_credentials:
192 self.set_header("Access-Control-Allow-Credentials", 'true')
192 self.set_header("Access-Control-Allow-Credentials", 'true')
193
193
194 def get_origin(self):
194 def get_origin(self):
195 # Handle WebSocket Origin naming convention differences
195 # Handle WebSocket Origin naming convention differences
196 # The difference between version 8 and 13 is that in 8 the
196 # The difference between version 8 and 13 is that in 8 the
197 # client sends a "Sec-Websocket-Origin" header and in 13 it's
197 # client sends a "Sec-Websocket-Origin" header and in 13 it's
198 # simply "Origin".
198 # simply "Origin".
199 if "Origin" in self.request.headers:
199 if "Origin" in self.request.headers:
200 origin = self.request.headers.get("Origin")
200 origin = self.request.headers.get("Origin")
201 else:
201 else:
202 origin = self.request.headers.get("Sec-Websocket-Origin", None)
202 origin = self.request.headers.get("Sec-Websocket-Origin", None)
203 return origin
203 return origin
204
204
205 #---------------------------------------------------------------
205 #---------------------------------------------------------------
206 # template rendering
206 # template rendering
207 #---------------------------------------------------------------
207 #---------------------------------------------------------------
208
208
209 def get_template(self, name):
209 def get_template(self, name):
210 """Return the jinja template object for a given name"""
210 """Return the jinja template object for a given name"""
211 return self.settings['jinja2_env'].get_template(name)
211 return self.settings['jinja2_env'].get_template(name)
212
212
213 def render_template(self, name, **ns):
213 def render_template(self, name, **ns):
214 ns.update(self.template_namespace)
214 ns.update(self.template_namespace)
215 template = self.get_template(name)
215 template = self.get_template(name)
216 return template.render(**ns)
216 return template.render(**ns)
217
217
218 @property
218 @property
219 def template_namespace(self):
219 def template_namespace(self):
220 return dict(
220 return dict(
221 base_url=self.base_url,
221 base_url=self.base_url,
222 ws_url=self.ws_url,
222 ws_url=self.ws_url,
223 logged_in=self.logged_in,
223 logged_in=self.logged_in,
224 login_available=self.login_available,
224 login_available=self.login_available,
225 static_url=self.static_url,
225 static_url=self.static_url,
226 )
226 )
227
227
228 def get_json_body(self):
228 def get_json_body(self):
229 """Return the body of the request as JSON data."""
229 """Return the body of the request as JSON data."""
230 if not self.request.body:
230 if not self.request.body:
231 return None
231 return None
232 # Do we need to call body.decode('utf-8') here?
232 # Do we need to call body.decode('utf-8') here?
233 body = self.request.body.strip().decode(u'utf-8')
233 body = self.request.body.strip().decode(u'utf-8')
234 try:
234 try:
235 model = json.loads(body)
235 model = json.loads(body)
236 except Exception:
236 except Exception:
237 self.log.debug("Bad JSON: %r", body)
237 self.log.debug("Bad JSON: %r", body)
238 self.log.error("Couldn't parse JSON", exc_info=True)
238 self.log.error("Couldn't parse JSON", exc_info=True)
239 raise web.HTTPError(400, u'Invalid JSON in body of request')
239 raise web.HTTPError(400, u'Invalid JSON in body of request')
240 return model
240 return model
241
241
242 def get_error_html(self, status_code, **kwargs):
242 def get_error_html(self, status_code, **kwargs):
243 """render custom error pages"""
243 """render custom error pages"""
244 exception = kwargs.get('exception')
244 exception = kwargs.get('exception')
245 message = ''
245 message = ''
246 status_message = responses.get(status_code, 'Unknown HTTP Error')
246 status_message = responses.get(status_code, 'Unknown HTTP Error')
247 if exception:
247 if exception:
248 # get the custom message, if defined
248 # get the custom message, if defined
249 try:
249 try:
250 message = exception.log_message % exception.args
250 message = exception.log_message % exception.args
251 except Exception:
251 except Exception:
252 pass
252 pass
253
253
254 # construct the custom reason, if defined
254 # construct the custom reason, if defined
255 reason = getattr(exception, 'reason', '')
255 reason = getattr(exception, 'reason', '')
256 if reason:
256 if reason:
257 status_message = reason
257 status_message = reason
258
258
259 # build template namespace
259 # build template namespace
260 ns = dict(
260 ns = dict(
261 status_code=status_code,
261 status_code=status_code,
262 status_message=status_message,
262 status_message=status_message,
263 message=message,
263 message=message,
264 exception=exception,
264 exception=exception,
265 )
265 )
266
266
267 # render the template
267 # render the template
268 try:
268 try:
269 html = self.render_template('%s.html' % status_code, **ns)
269 html = self.render_template('%s.html' % status_code, **ns)
270 except TemplateNotFound:
270 except TemplateNotFound:
271 self.log.debug("No template for %d", status_code)
271 self.log.debug("No template for %d", status_code)
272 html = self.render_template('error.html', **ns)
272 html = self.render_template('error.html', **ns)
273 return html
273 return html
274
274
275
275
276 class Template404(IPythonHandler):
276 class Template404(IPythonHandler):
277 """Render our 404 template"""
277 """Render our 404 template"""
278 def prepare(self):
278 def prepare(self):
279 raise web.HTTPError(404)
279 raise web.HTTPError(404)
280
280
281
281
282 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
282 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
283 """static files should only be accessible when logged in"""
283 """static files should only be accessible when logged in"""
284
284
285 @web.authenticated
285 @web.authenticated
286 def get(self, path):
286 def get(self, path):
287 if os.path.splitext(path)[1] == '.ipynb':
287 if os.path.splitext(path)[1] == '.ipynb':
288 name = os.path.basename(path)
288 name = os.path.basename(path)
289 self.set_header('Content-Type', 'application/json')
289 self.set_header('Content-Type', 'application/json')
290 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
290 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
291
291
292 return web.StaticFileHandler.get(self, path)
292 return web.StaticFileHandler.get(self, path)
293
293
294 def compute_etag(self):
294 def compute_etag(self):
295 return None
295 return None
296
296
297 def validate_absolute_path(self, root, absolute_path):
297 def validate_absolute_path(self, root, absolute_path):
298 """Validate and return the absolute path.
298 """Validate and return the absolute path.
299
299
300 Requires tornado 3.1
300 Requires tornado 3.1
301
301
302 Adding to tornado's own handling, forbids the serving of hidden files.
302 Adding to tornado's own handling, forbids the serving of hidden files.
303 """
303 """
304 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
304 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
305 abs_root = os.path.abspath(root)
305 abs_root = os.path.abspath(root)
306 if is_hidden(abs_path, abs_root):
306 if is_hidden(abs_path, abs_root):
307 self.log.info("Refusing to serve hidden file, via 404 Error")
307 self.log.info("Refusing to serve hidden file, via 404 Error")
308 raise web.HTTPError(404)
308 raise web.HTTPError(404)
309 return abs_path
309 return abs_path
310
310
311
311
312 def json_errors(method):
312 def json_errors(method):
313 """Decorate methods with this to return GitHub style JSON errors.
313 """Decorate methods with this to return GitHub style JSON errors.
314
314
315 This should be used on any JSON API on any handler method that can raise HTTPErrors.
315 This should be used on any JSON API on any handler method that can raise HTTPErrors.
316
316
317 This will grab the latest HTTPError exception using sys.exc_info
317 This will grab the latest HTTPError exception using sys.exc_info
318 and then:
318 and then:
319
319
320 1. Set the HTTP status code based on the HTTPError
320 1. Set the HTTP status code based on the HTTPError
321 2. Create and return a JSON body with a message field describing
321 2. Create and return a JSON body with a message field describing
322 the error in a human readable form.
322 the error in a human readable form.
323 """
323 """
324 @functools.wraps(method)
324 @functools.wraps(method)
325 def wrapper(self, *args, **kwargs):
325 def wrapper(self, *args, **kwargs):
326 try:
326 try:
327 result = method(self, *args, **kwargs)
327 result = method(self, *args, **kwargs)
328 except web.HTTPError as e:
328 except web.HTTPError as e:
329 status = e.status_code
329 status = e.status_code
330 message = e.log_message
330 message = e.log_message
331 self.log.warn(message)
331 self.log.warn(message)
332 self.set_status(e.status_code)
332 self.set_status(e.status_code)
333 self.finish(json.dumps(dict(message=message)))
333 self.finish(json.dumps(dict(message=message)))
334 except Exception:
334 except Exception:
335 self.log.error("Unhandled error in API request", exc_info=True)
335 self.log.error("Unhandled error in API request", exc_info=True)
336 status = 500
336 status = 500
337 message = "Unknown server error"
337 message = "Unknown server error"
338 t, value, tb = sys.exc_info()
338 t, value, tb = sys.exc_info()
339 self.set_status(status)
339 self.set_status(status)
340 tb_text = ''.join(traceback.format_exception(t, value, tb))
340 tb_text = ''.join(traceback.format_exception(t, value, tb))
341 reply = dict(message=message, traceback=tb_text)
341 reply = dict(message=message, traceback=tb_text)
342 self.finish(json.dumps(reply))
342 self.finish(json.dumps(reply))
343 else:
343 else:
344 return result
344 return result
345 return wrapper
345 return wrapper
346
346
347
347
348
348
349 #-----------------------------------------------------------------------------
349 #-----------------------------------------------------------------------------
350 # File handler
350 # File handler
351 #-----------------------------------------------------------------------------
351 #-----------------------------------------------------------------------------
352
352
353 # to minimize subclass changes:
353 # to minimize subclass changes:
354 HTTPError = web.HTTPError
354 HTTPError = web.HTTPError
355
355
356 class FileFindHandler(web.StaticFileHandler):
356 class FileFindHandler(web.StaticFileHandler):
357 """subclass of StaticFileHandler for serving files from a search path"""
357 """subclass of StaticFileHandler for serving files from a search path"""
358
358
359 # cache search results, don't search for files more than once
359 # cache search results, don't search for files more than once
360 _static_paths = {}
360 _static_paths = {}
361
361
362 def initialize(self, path, default_filename=None):
362 def initialize(self, path, default_filename=None):
363 if isinstance(path, string_types):
363 if isinstance(path, string_types):
364 path = [path]
364 path = [path]
365
365
366 self.root = tuple(
366 self.root = tuple(
367 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
367 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
368 )
368 )
369 self.default_filename = default_filename
369 self.default_filename = default_filename
370
370
371 def compute_etag(self):
371 def compute_etag(self):
372 return None
372 return None
373
373
374 @classmethod
374 @classmethod
375 def get_absolute_path(cls, roots, path):
375 def get_absolute_path(cls, roots, path):
376 """locate a file to serve on our static file search path"""
376 """locate a file to serve on our static file search path"""
377 with cls._lock:
377 with cls._lock:
378 if path in cls._static_paths:
378 if path in cls._static_paths:
379 return cls._static_paths[path]
379 return cls._static_paths[path]
380 try:
380 try:
381 abspath = os.path.abspath(filefind(path, roots))
381 abspath = os.path.abspath(filefind(path, roots))
382 except IOError:
382 except IOError:
383 # IOError means not found
383 # IOError means not found
384 return ''
384 return ''
385
385
386 cls._static_paths[path] = abspath
386 cls._static_paths[path] = abspath
387 return abspath
387 return abspath
388
388
389 def validate_absolute_path(self, root, absolute_path):
389 def validate_absolute_path(self, root, absolute_path):
390 """check if the file should be served (raises 404, 403, etc.)"""
390 """check if the file should be served (raises 404, 403, etc.)"""
391 if absolute_path == '':
391 if absolute_path == '':
392 raise web.HTTPError(404)
392 raise web.HTTPError(404)
393
393
394 for root in self.root:
394 for root in self.root:
395 if (absolute_path + os.sep).startswith(root):
395 if (absolute_path + os.sep).startswith(root):
396 break
396 break
397
397
398 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
398 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
399
399
400
400
401 class TrailingSlashHandler(web.RequestHandler):
401 class TrailingSlashHandler(web.RequestHandler):
402 """Simple redirect handler that strips trailing slashes
402 """Simple redirect handler that strips trailing slashes
403
403
404 This should be the first, highest priority handler.
404 This should be the first, highest priority handler.
405 """
405 """
406
406
407 SUPPORTED_METHODS = ['GET']
407 SUPPORTED_METHODS = ['GET']
408
408
409 def get(self):
409 def get(self):
410 self.redirect(self.request.uri.rstrip('/'))
410 self.redirect(self.request.uri.rstrip('/'))
411
411
412 #-----------------------------------------------------------------------------
412 #-----------------------------------------------------------------------------
413 # URL pattern fragments for re-use
413 # URL pattern fragments for re-use
414 #-----------------------------------------------------------------------------
414 #-----------------------------------------------------------------------------
415
415
416 path_regex = r"(?P<path>(?:/.*)*)"
416 path_regex = r"(?P<path>(?:/.*)*)"
417 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
417 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
418 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
418 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
419
419
420 #-----------------------------------------------------------------------------
420 #-----------------------------------------------------------------------------
421 # URL to handler mappings
421 # URL to handler mappings
422 #-----------------------------------------------------------------------------
422 #-----------------------------------------------------------------------------
423
423
424
424
425 default_handlers = [
425 default_handlers = [
426 (r".*/", TrailingSlashHandler)
426 (r".*/", TrailingSlashHandler)
427 ]
427 ]
@@ -1,137 +1,137 b''
1 import io
1 import io
2 import os
2 import os
3 import zipfile
3 import zipfile
4
4
5 from tornado import web
5 from tornado import web
6
6
7 from ..base.handlers import IPythonHandler, notebook_path_regex
7 from ..base.handlers import IPythonHandler, notebook_path_regex
8 from IPython.nbformat.current import to_notebook_json
8 from IPython.nbformat.current import to_notebook_json
9
9
10 from IPython.utils.py3compat import cast_bytes
10 from IPython.utils.py3compat import cast_bytes
11
11
12 def find_resource_files(output_files_dir):
12 def find_resource_files(output_files_dir):
13 files = []
13 files = []
14 for dirpath, dirnames, filenames in os.walk(output_files_dir):
14 for dirpath, dirnames, filenames in os.walk(output_files_dir):
15 files.extend([os.path.join(dirpath, f) for f in filenames])
15 files.extend([os.path.join(dirpath, f) for f in filenames])
16 return files
16 return files
17
17
18 def respond_zip(handler, name, output, resources):
18 def respond_zip(handler, name, output, resources):
19 """Zip up the output and resource files and respond with the zip file.
19 """Zip up the output and resource files and respond with the zip file.
20
20
21 Returns True if it has served a zip file, False if there are no resource
21 Returns True if it has served a zip file, False if there are no resource
22 files, in which case we serve the plain output file.
22 files, in which case we serve the plain output file.
23 """
23 """
24 # Check if we have resource files we need to zip
24 # Check if we have resource files we need to zip
25 output_files = resources.get('outputs', None)
25 output_files = resources.get('outputs', None)
26 if not output_files:
26 if not output_files:
27 return False
27 return False
28
28
29 # Headers
29 # Headers
30 zip_filename = os.path.splitext(name)[0] + '.zip'
30 zip_filename = os.path.splitext(name)[0] + '.zip'
31 handler.set_header('Content-Disposition',
31 handler.set_header('Content-Disposition',
32 'attachment; filename="%s"' % zip_filename)
32 'attachment; filename="%s"' % zip_filename)
33 handler.set_header('Content-Type', 'application/zip')
33 handler.set_header('Content-Type', 'application/zip')
34
34
35 # Prepare the zip file
35 # Prepare the zip file
36 buffer = io.BytesIO()
36 buffer = io.BytesIO()
37 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
37 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
38 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
38 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
39 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
39 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
40 for filename, data in output_files.items():
40 for filename, data in output_files.items():
41 zipf.writestr(os.path.basename(filename), data)
41 zipf.writestr(os.path.basename(filename), data)
42 zipf.close()
42 zipf.close()
43
43
44 handler.finish(buffer.getvalue())
44 handler.finish(buffer.getvalue())
45 return True
45 return True
46
46
47 def get_exporter(format, **kwargs):
47 def get_exporter(format, **kwargs):
48 """get an exporter, raising appropriate errors"""
48 """get an exporter, raising appropriate errors"""
49 # if this fails, will raise 500
49 # if this fails, will raise 500
50 try:
50 try:
51 from IPython.nbconvert.exporters.export import exporter_map
51 from IPython.nbconvert.exporters.export import exporter_map
52 except ImportError as e:
52 except ImportError as e:
53 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
53 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
54
54
55 try:
55 try:
56 Exporter = exporter_map[format]
56 Exporter = exporter_map[format]
57 except KeyError:
57 except KeyError:
58 # should this be 400?
58 # should this be 400?
59 raise web.HTTPError(404, u"No exporter for format: %s" % format)
59 raise web.HTTPError(404, u"No exporter for format: %s" % format)
60
60
61 try:
61 try:
62 return Exporter(**kwargs)
62 return Exporter(**kwargs)
63 except Exception as e:
63 except Exception as e:
64 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
64 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
65
65
66 class NbconvertFileHandler(IPythonHandler):
66 class NbconvertFileHandler(IPythonHandler):
67
67
68 SUPPORTED_METHODS = ('GET',)
68 SUPPORTED_METHODS = ('GET',)
69
69
70 @web.authenticated
70 @web.authenticated
71 def get(self, format, path='', name=None):
71 def get(self, format, path='', name=None):
72
72
73 exporter = get_exporter(format, config=self.config, log=self.log)
73 exporter = get_exporter(format, config=self.config, log=self.log)
74
74
75 path = path.strip('/')
75 path = path.strip('/')
76 model = self.notebook_manager.get_notebook(name=name, path=path)
76 model = self.contents_manager.get(name=name, path=path)
77
77
78 self.set_header('Last-Modified', model['last_modified'])
78 self.set_header('Last-Modified', model['last_modified'])
79
79
80 try:
80 try:
81 output, resources = exporter.from_notebook_node(model['content'])
81 output, resources = exporter.from_notebook_node(model['content'])
82 except Exception as e:
82 except Exception as e:
83 raise web.HTTPError(500, "nbconvert failed: %s" % e)
83 raise web.HTTPError(500, "nbconvert failed: %s" % e)
84
84
85 if respond_zip(self, name, output, resources):
85 if respond_zip(self, name, output, resources):
86 return
86 return
87
87
88 # Force download if requested
88 # Force download if requested
89 if self.get_argument('download', 'false').lower() == 'true':
89 if self.get_argument('download', 'false').lower() == 'true':
90 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
90 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
91 self.set_header('Content-Disposition',
91 self.set_header('Content-Disposition',
92 'attachment; filename="%s"' % filename)
92 'attachment; filename="%s"' % filename)
93
93
94 # MIME type
94 # MIME type
95 if exporter.output_mimetype:
95 if exporter.output_mimetype:
96 self.set_header('Content-Type',
96 self.set_header('Content-Type',
97 '%s; charset=utf-8' % exporter.output_mimetype)
97 '%s; charset=utf-8' % exporter.output_mimetype)
98
98
99 self.finish(output)
99 self.finish(output)
100
100
101 class NbconvertPostHandler(IPythonHandler):
101 class NbconvertPostHandler(IPythonHandler):
102 SUPPORTED_METHODS = ('POST',)
102 SUPPORTED_METHODS = ('POST',)
103
103
104 @web.authenticated
104 @web.authenticated
105 def post(self, format):
105 def post(self, format):
106 exporter = get_exporter(format, config=self.config)
106 exporter = get_exporter(format, config=self.config)
107
107
108 model = self.get_json_body()
108 model = self.get_json_body()
109 nbnode = to_notebook_json(model['content'])
109 nbnode = to_notebook_json(model['content'])
110
110
111 try:
111 try:
112 output, resources = exporter.from_notebook_node(nbnode)
112 output, resources = exporter.from_notebook_node(nbnode)
113 except Exception as e:
113 except Exception as e:
114 raise web.HTTPError(500, "nbconvert failed: %s" % e)
114 raise web.HTTPError(500, "nbconvert failed: %s" % e)
115
115
116 if respond_zip(self, nbnode.metadata.name, output, resources):
116 if respond_zip(self, nbnode.metadata.name, output, resources):
117 return
117 return
118
118
119 # MIME type
119 # MIME type
120 if exporter.output_mimetype:
120 if exporter.output_mimetype:
121 self.set_header('Content-Type',
121 self.set_header('Content-Type',
122 '%s; charset=utf-8' % exporter.output_mimetype)
122 '%s; charset=utf-8' % exporter.output_mimetype)
123
123
124 self.finish(output)
124 self.finish(output)
125
125
126 #-----------------------------------------------------------------------------
126 #-----------------------------------------------------------------------------
127 # URL to handler mappings
127 # URL to handler mappings
128 #-----------------------------------------------------------------------------
128 #-----------------------------------------------------------------------------
129
129
130 _format_regex = r"(?P<format>\w+)"
130 _format_regex = r"(?P<format>\w+)"
131
131
132
132
133 default_handlers = [
133 default_handlers = [
134 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
134 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
135 NbconvertFileHandler),
135 NbconvertFileHandler),
136 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
136 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
137 ]
137 ]
@@ -1,129 +1,129 b''
1 # coding: utf-8
1 # coding: utf-8
2 import base64
2 import base64
3 import io
3 import io
4 import json
4 import json
5 import os
5 import os
6 from os.path import join as pjoin
6 from os.path import join as pjoin
7 import shutil
7 import shutil
8
8
9 import requests
9 import requests
10
10
11 from IPython.html.utils import url_path_join
11 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
14 new_heading_cell, new_code_cell,
14 new_heading_cell, new_code_cell,
15 new_output)
15 new_output)
16
16
17 from IPython.testing.decorators import onlyif_cmds_exist
17 from IPython.testing.decorators import onlyif_cmds_exist
18
18
19
19
20 class NbconvertAPI(object):
20 class NbconvertAPI(object):
21 """Wrapper for nbconvert API calls."""
21 """Wrapper for nbconvert API calls."""
22 def __init__(self, base_url):
22 def __init__(self, base_url):
23 self.base_url = base_url
23 self.base_url = base_url
24
24
25 def _req(self, verb, path, body=None, params=None):
25 def _req(self, verb, path, body=None, params=None):
26 response = requests.request(verb,
26 response = requests.request(verb,
27 url_path_join(self.base_url, 'nbconvert', path),
27 url_path_join(self.base_url, 'nbconvert', path),
28 data=body, params=params,
28 data=body, params=params,
29 )
29 )
30 response.raise_for_status()
30 response.raise_for_status()
31 return response
31 return response
32
32
33 def from_file(self, format, path, name, download=False):
33 def from_file(self, format, path, name, download=False):
34 return self._req('GET', url_path_join(format, path, name),
34 return self._req('GET', url_path_join(format, path, name),
35 params={'download':download})
35 params={'download':download})
36
36
37 def from_post(self, format, nbmodel):
37 def from_post(self, format, nbmodel):
38 body = json.dumps(nbmodel)
38 body = json.dumps(nbmodel)
39 return self._req('POST', format, body)
39 return self._req('POST', format, body)
40
40
41 def list_formats(self):
41 def list_formats(self):
42 return self._req('GET', '')
42 return self._req('GET', '')
43
43
44 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
44 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
45 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
45 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
46 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82')
46 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82')
47
47
48 class APITest(NotebookTestBase):
48 class APITest(NotebookTestBase):
49 def setUp(self):
49 def setUp(self):
50 nbdir = self.notebook_dir.name
50 nbdir = self.notebook_dir.name
51
51
52 if not os.path.isdir(pjoin(nbdir, 'foo')):
52 if not os.path.isdir(pjoin(nbdir, 'foo')):
53 os.mkdir(pjoin(nbdir, 'foo'))
53 os.mkdir(pjoin(nbdir, 'foo'))
54
54
55 nb = new_notebook(name='testnb')
55 nb = new_notebook(name='testnb')
56
56
57 ws = new_worksheet()
57 ws = new_worksheet()
58 nb.worksheets = [ws]
58 nb.worksheets = [ws]
59 ws.cells.append(new_heading_cell(u'Created by test Β³'))
59 ws.cells.append(new_heading_cell(u'Created by test Β³'))
60 cc1 = new_code_cell(input=u'print(2*6)')
60 cc1 = new_code_cell(input=u'print(2*6)')
61 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
61 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
62 cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout'))
62 cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout'))
63 ws.cells.append(cc1)
63 ws.cells.append(cc1)
64
64
65 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
65 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
66 encoding='utf-8') as f:
66 encoding='utf-8') as f:
67 write(nb, f, format='ipynb')
67 write(nb, f, format='ipynb')
68
68
69 self.nbconvert_api = NbconvertAPI(self.base_url())
69 self.nbconvert_api = NbconvertAPI(self.base_url())
70
70
71 def tearDown(self):
71 def tearDown(self):
72 nbdir = self.notebook_dir.name
72 nbdir = self.notebook_dir.name
73
73
74 for dname in ['foo']:
74 for dname in ['foo']:
75 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
75 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
76
76
77 @onlyif_cmds_exist('pandoc')
77 @onlyif_cmds_exist('pandoc')
78 def test_from_file(self):
78 def test_from_file(self):
79 r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
79 r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
80 self.assertEqual(r.status_code, 200)
80 self.assertEqual(r.status_code, 200)
81 self.assertIn(u'text/html', r.headers['Content-Type'])
81 self.assertIn(u'text/html', r.headers['Content-Type'])
82 self.assertIn(u'Created by test', r.text)
82 self.assertIn(u'Created by test', r.text)
83 self.assertIn(u'print', r.text)
83 self.assertIn(u'print', r.text)
84
84
85 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
85 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
86 self.assertIn(u'text/x-python', r.headers['Content-Type'])
86 self.assertIn(u'text/x-python', r.headers['Content-Type'])
87 self.assertIn(u'print(2*6)', r.text)
87 self.assertIn(u'print(2*6)', r.text)
88
88
89 @onlyif_cmds_exist('pandoc')
89 @onlyif_cmds_exist('pandoc')
90 def test_from_file_404(self):
90 def test_from_file_404(self):
91 with assert_http_error(404):
91 with assert_http_error(404):
92 self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
92 self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
93
93
94 @onlyif_cmds_exist('pandoc')
94 @onlyif_cmds_exist('pandoc')
95 def test_from_file_download(self):
95 def test_from_file_download(self):
96 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
96 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
97 content_disposition = r.headers['Content-Disposition']
97 content_disposition = r.headers['Content-Disposition']
98 self.assertIn('attachment', content_disposition)
98 self.assertIn('attachment', content_disposition)
99 self.assertIn('testnb.py', content_disposition)
99 self.assertIn('testnb.py', content_disposition)
100
100
101 @onlyif_cmds_exist('pandoc')
101 @onlyif_cmds_exist('pandoc')
102 def test_from_file_zip(self):
102 def test_from_file_zip(self):
103 r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
103 r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
104 self.assertIn(u'application/zip', r.headers['Content-Type'])
104 self.assertIn(u'application/zip', r.headers['Content-Type'])
105 self.assertIn(u'.zip', r.headers['Content-Disposition'])
105 self.assertIn(u'.zip', r.headers['Content-Disposition'])
106
106
107 @onlyif_cmds_exist('pandoc')
107 @onlyif_cmds_exist('pandoc')
108 def test_from_post(self):
108 def test_from_post(self):
109 nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb')
109 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
110 nbmodel = requests.get(nbmodel_url).json()
110 nbmodel = requests.get(nbmodel_url).json()
111
111
112 r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
112 r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
113 self.assertEqual(r.status_code, 200)
113 self.assertEqual(r.status_code, 200)
114 self.assertIn(u'text/html', r.headers['Content-Type'])
114 self.assertIn(u'text/html', r.headers['Content-Type'])
115 self.assertIn(u'Created by test', r.text)
115 self.assertIn(u'Created by test', r.text)
116 self.assertIn(u'print', r.text)
116 self.assertIn(u'print', r.text)
117
117
118 r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
118 r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
119 self.assertIn(u'text/x-python', r.headers['Content-Type'])
119 self.assertIn(u'text/x-python', r.headers['Content-Type'])
120 self.assertIn(u'print(2*6)', r.text)
120 self.assertIn(u'print(2*6)', r.text)
121
121
122 @onlyif_cmds_exist('pandoc')
122 @onlyif_cmds_exist('pandoc')
123 def test_from_post_zip(self):
123 def test_from_post_zip(self):
124 nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb')
124 nbmodel_url = url_path_join(self.base_url(), 'api/contents/foo/testnb.ipynb')
125 nbmodel = requests.get(nbmodel_url).json()
125 nbmodel = requests.get(nbmodel_url).json()
126
126
127 r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
127 r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
128 self.assertIn(u'application/zip', r.headers['Content-Type'])
128 self.assertIn(u'application/zip', r.headers['Content-Type'])
129 self.assertIn(u'.zip', r.headers['Content-Disposition'])
129 self.assertIn(u'.zip', r.headers['Content-Disposition'])
@@ -1,90 +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, notebook_path_regex, path_regex
23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
24 from ..utils import url_path_join, url_escape
24 from ..utils import url_path_join, url_escape
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Handlers
27 # Handlers
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30
30
31 class NotebookHandler(IPythonHandler):
31 class NotebookHandler(IPythonHandler):
32
32
33 @web.authenticated
33 @web.authenticated
34 def get(self, path='', name=None):
34 def get(self, path='', name=None):
35 """get renders the notebook template if a name is given, or
35 """get renders the notebook template if a name is given, or
36 redirects to the '/files/' handler if the name is not given."""
36 redirects to the '/files/' handler if the name is not given."""
37 path = path.strip('/')
37 path = path.strip('/')
38 nbm = self.notebook_manager
38 cm = self.contents_manager
39 if name is None:
39 if name is None:
40 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)
41
41
42 # a .ipynb filename was given
42 # a .ipynb filename was given
43 if not nbm.notebook_exists(name, path):
43 if not cm.file_exists(name, path):
44 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))
45 name = url_escape(name)
45 name = url_escape(name)
46 path = url_escape(path)
46 path = url_escape(path)
47 self.write(self.render_template('notebook.html',
47 self.write(self.render_template('notebook.html',
48 project=self.project_dir,
48 project=self.project_dir,
49 notebook_path=path,
49 notebook_path=path,
50 notebook_name=name,
50 notebook_name=name,
51 kill_kernel=False,
51 kill_kernel=False,
52 mathjax_url=self.mathjax_url,
52 mathjax_url=self.mathjax_url,
53 )
53 )
54 )
54 )
55
55
56 class NotebookRedirectHandler(IPythonHandler):
56 class NotebookRedirectHandler(IPythonHandler):
57 def get(self, path=''):
57 def get(self, path=''):
58 nbm = self.notebook_manager
58 cm = self.contents_manager
59 if nbm.path_exists(path):
59 if cm.path_exists(path):
60 # it's a *directory*, redirect to /tree
60 # it's a *directory*, redirect to /tree
61 url = url_path_join(self.base_url, 'tree', path)
61 url = url_path_join(self.base_url, 'tree', path)
62 else:
62 else:
63 # otherwise, redirect to /files
63 # otherwise, redirect to /files
64 if '/files/' in path:
64 if '/files/' in path:
65 # redirect without files/ iff it would 404
65 # redirect without files/ iff it would 404
66 # this preserves pre-2.0-style 'files/' links
66 # this preserves pre-2.0-style 'files/' links
67 # FIXME: this is hardcoded based on notebook_path,
67 # FIXME: this is hardcoded based on notebook_path,
68 # but so is the files handler itself,
68 # but so is the files handler itself,
69 # so it should work until both are cleaned up.
69 # so it should work until both are cleaned up.
70 parts = path.split('/')
70 parts = path.split('/')
71 files_path = os.path.join(nbm.notebook_dir, *parts)
71 files_path = os.path.join(cm.root_dir, *parts)
72 if not os.path.exists(files_path):
72 if not os.path.exists(files_path):
73 self.log.warn("Deprecated files/ URL: %s", path)
73 self.log.warn("Deprecated files/ URL: %s", path)
74 path = path.replace('/files/', '/', 1)
74 path = path.replace('/files/', '/', 1)
75
75
76 url = url_path_join(self.base_url, 'files', path)
76 url = url_path_join(self.base_url, 'files', path)
77 url = url_escape(url)
77 url = url_escape(url)
78 self.log.debug("Redirecting %s to %s", self.request.path, url)
78 self.log.debug("Redirecting %s to %s", self.request.path, url)
79 self.redirect(url)
79 self.redirect(url)
80
80
81 #-----------------------------------------------------------------------------
81 #-----------------------------------------------------------------------------
82 # URL to handler mappings
82 # URL to handler mappings
83 #-----------------------------------------------------------------------------
83 #-----------------------------------------------------------------------------
84
84
85
85
86 default_handlers = [
86 default_handlers = [
87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
89 ]
89 ]
90
90
@@ -1,943 +1,940 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import errno
10 import errno
11 import io
11 import io
12 import json
12 import json
13 import logging
13 import logging
14 import os
14 import os
15 import random
15 import random
16 import re
16 import re
17 import select
17 import select
18 import signal
18 import signal
19 import socket
19 import socket
20 import sys
20 import sys
21 import threading
21 import threading
22 import time
22 import time
23 import webbrowser
23 import webbrowser
24
24
25
25
26 # check for pyzmq 2.1.11
26 # check for pyzmq 2.1.11
27 from IPython.utils.zmqrelated import check_for_zmq
27 from IPython.utils.zmqrelated import check_for_zmq
28 check_for_zmq('2.1.11', 'IPython.html')
28 check_for_zmq('2.1.11', 'IPython.html')
29
29
30 from jinja2 import Environment, FileSystemLoader
30 from jinja2 import Environment, FileSystemLoader
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 ioloop.install()
35 ioloop.install()
36
36
37 # check for tornado 3.1.0
37 # check for tornado 3.1.0
38 msg = "The IPython Notebook requires tornado >= 3.1.0"
38 msg = "The IPython Notebook requires tornado >= 3.1.0"
39 try:
39 try:
40 import tornado
40 import tornado
41 except ImportError:
41 except ImportError:
42 raise ImportError(msg)
42 raise ImportError(msg)
43 try:
43 try:
44 version_info = tornado.version_info
44 version_info = tornado.version_info
45 except AttributeError:
45 except AttributeError:
46 raise ImportError(msg + ", but you have < 1.1.0")
46 raise ImportError(msg + ", but you have < 1.1.0")
47 if version_info < (3,1,0):
47 if version_info < (3,1,0):
48 raise ImportError(msg + ", but you have %s" % tornado.version)
48 raise ImportError(msg + ", but you have %s" % tornado.version)
49
49
50 from tornado import httpserver
50 from tornado import httpserver
51 from tornado import web
51 from tornado import web
52 from tornado.log import LogFormatter
52 from tornado.log import LogFormatter
53
53
54 from IPython.html import DEFAULT_STATIC_FILES_PATH
54 from IPython.html import DEFAULT_STATIC_FILES_PATH
55 from .base.handlers import Template404
55 from .base.handlers import Template404
56 from .log import log_request
56 from .log import log_request
57 from .services.kernels.kernelmanager import MappingKernelManager
57 from .services.kernels.kernelmanager import MappingKernelManager
58 from .services.notebooks.nbmanager import NotebookManager
58 from .services.contents.manager import ContentsManager
59 from .services.notebooks.filenbmanager import FileNotebookManager
59 from .services.contents.filemanager import FileContentsManager
60 from .services.clusters.clustermanager import ClusterManager
60 from .services.clusters.clustermanager import ClusterManager
61 from .services.sessions.sessionmanager import SessionManager
61 from .services.sessions.sessionmanager import SessionManager
62
62
63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
64
64
65 from IPython.config import Config
65 from IPython.config import Config
66 from IPython.config.application import catch_config_error, boolean_flag
66 from IPython.config.application import catch_config_error, boolean_flag
67 from IPython.core.application import (
67 from IPython.core.application import (
68 BaseIPythonApplication, base_flags, base_aliases,
68 BaseIPythonApplication, base_flags, base_aliases,
69 )
69 )
70 from IPython.core.profiledir import ProfileDir
70 from IPython.core.profiledir import ProfileDir
71 from IPython.kernel import KernelManager
71 from IPython.kernel import KernelManager
72 from IPython.kernel.kernelspec import KernelSpecManager
72 from IPython.kernel.kernelspec import KernelSpecManager
73 from IPython.kernel.zmq.session import default_secure, Session
73 from IPython.kernel.zmq.session import default_secure, Session
74 from IPython.nbformat.sign import NotebookNotary
74 from IPython.nbformat.sign import NotebookNotary
75 from IPython.utils.importstring import import_item
75 from IPython.utils.importstring import import_item
76 from IPython.utils import submodule
76 from IPython.utils import submodule
77 from IPython.utils.process import check_pid
77 from IPython.utils.process import check_pid
78 from IPython.utils.traitlets import (
78 from IPython.utils.traitlets import (
79 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
79 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
80 DottedObjectName, TraitError,
80 DottedObjectName, TraitError,
81 )
81 )
82 from IPython.utils import py3compat
82 from IPython.utils import py3compat
83 from IPython.utils.path import filefind, get_ipython_dir
83 from IPython.utils.path import filefind, get_ipython_dir
84
84
85 from .utils import url_path_join
85 from .utils import url_path_join
86
86
87 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
88 # Module globals
88 # Module globals
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90
90
91 _examples = """
91 _examples = """
92 ipython notebook # start the notebook
92 ipython notebook # start the notebook
93 ipython notebook --profile=sympy # use the sympy profile
93 ipython notebook --profile=sympy # use the sympy profile
94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
95 """
95 """
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Helper functions
98 # Helper functions
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 def random_ports(port, n):
101 def random_ports(port, n):
102 """Generate a list of n random ports near the given port.
102 """Generate a list of n random ports near the given port.
103
103
104 The first 5 ports will be sequential, and the remaining n-5 will be
104 The first 5 ports will be sequential, and the remaining n-5 will be
105 randomly selected in the range [port-2*n, port+2*n].
105 randomly selected in the range [port-2*n, port+2*n].
106 """
106 """
107 for i in range(min(5, n)):
107 for i in range(min(5, n)):
108 yield port + i
108 yield port + i
109 for i in range(n-5):
109 for i in range(n-5):
110 yield max(1, port + random.randint(-2*n, 2*n))
110 yield max(1, port + random.randint(-2*n, 2*n))
111
111
112 def load_handlers(name):
112 def load_handlers(name):
113 """Load the (URL pattern, handler) tuples for each component."""
113 """Load the (URL pattern, handler) tuples for each component."""
114 name = 'IPython.html.' + name
114 name = 'IPython.html.' + name
115 mod = __import__(name, fromlist=['default_handlers'])
115 mod = __import__(name, fromlist=['default_handlers'])
116 return mod.default_handlers
116 return mod.default_handlers
117
117
118 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
119 # The Tornado web application
119 # The Tornado web application
120 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
121
121
122 class NotebookWebApplication(web.Application):
122 class NotebookWebApplication(web.Application):
123
123
124 def __init__(self, ipython_app, kernel_manager, notebook_manager,
124 def __init__(self, ipython_app, kernel_manager, contents_manager,
125 cluster_manager, session_manager, kernel_spec_manager, log,
125 cluster_manager, session_manager, kernel_spec_manager, log,
126 base_url, settings_overrides, jinja_env_options):
126 base_url, settings_overrides, jinja_env_options):
127
127
128 settings = self.init_settings(
128 settings = self.init_settings(
129 ipython_app, kernel_manager, notebook_manager, cluster_manager,
129 ipython_app, kernel_manager, contents_manager, cluster_manager,
130 session_manager, kernel_spec_manager, log, base_url,
130 session_manager, kernel_spec_manager, log, base_url,
131 settings_overrides, jinja_env_options)
131 settings_overrides, jinja_env_options)
132 handlers = self.init_handlers(settings)
132 handlers = self.init_handlers(settings)
133
133
134 super(NotebookWebApplication, self).__init__(handlers, **settings)
134 super(NotebookWebApplication, self).__init__(handlers, **settings)
135
135
136 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
136 def init_settings(self, ipython_app, kernel_manager, contents_manager,
137 cluster_manager, session_manager, kernel_spec_manager,
137 cluster_manager, session_manager, kernel_spec_manager,
138 log, base_url, settings_overrides,
138 log, base_url, settings_overrides,
139 jinja_env_options=None):
139 jinja_env_options=None):
140 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
140 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
141 # base_url will always be unicode, which will in turn
141 # base_url will always be unicode, which will in turn
142 # make the patterns unicode, and ultimately result in unicode
142 # make the patterns unicode, and ultimately result in unicode
143 # keys in kwargs to handler._execute(**kwargs) in tornado.
143 # keys in kwargs to handler._execute(**kwargs) in tornado.
144 # This enforces that base_url be ascii in that situation.
144 # This enforces that base_url be ascii in that situation.
145 #
145 #
146 # Note that the URLs these patterns check against are escaped,
146 # Note that the URLs these patterns check against are escaped,
147 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
147 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
148 base_url = py3compat.unicode_to_str(base_url, 'ascii')
148 base_url = py3compat.unicode_to_str(base_url, 'ascii')
149 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
149 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
150 jenv_opt = jinja_env_options if jinja_env_options else {}
150 jenv_opt = jinja_env_options if jinja_env_options else {}
151 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
151 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
152 settings = dict(
152 settings = dict(
153 # basics
153 # basics
154 log_function=log_request,
154 log_function=log_request,
155 base_url=base_url,
155 base_url=base_url,
156 template_path=template_path,
156 template_path=template_path,
157 static_path=ipython_app.static_file_path,
157 static_path=ipython_app.static_file_path,
158 static_handler_class = FileFindHandler,
158 static_handler_class = FileFindHandler,
159 static_url_prefix = url_path_join(base_url,'/static/'),
159 static_url_prefix = url_path_join(base_url,'/static/'),
160
160
161 # authentication
161 # authentication
162 cookie_secret=ipython_app.cookie_secret,
162 cookie_secret=ipython_app.cookie_secret,
163 login_url=url_path_join(base_url,'/login'),
163 login_url=url_path_join(base_url,'/login'),
164 password=ipython_app.password,
164 password=ipython_app.password,
165
165
166 # managers
166 # managers
167 kernel_manager=kernel_manager,
167 kernel_manager=kernel_manager,
168 notebook_manager=notebook_manager,
168 contents_manager=contents_manager,
169 cluster_manager=cluster_manager,
169 cluster_manager=cluster_manager,
170 session_manager=session_manager,
170 session_manager=session_manager,
171 kernel_spec_manager=kernel_spec_manager,
171 kernel_spec_manager=kernel_spec_manager,
172
172
173 # IPython stuff
173 # IPython stuff
174 nbextensions_path = ipython_app.nbextensions_path,
174 nbextensions_path = ipython_app.nbextensions_path,
175 websocket_url=ipython_app.websocket_url,
175 websocket_url=ipython_app.websocket_url,
176 mathjax_url=ipython_app.mathjax_url,
176 mathjax_url=ipython_app.mathjax_url,
177 config=ipython_app.config,
177 config=ipython_app.config,
178 jinja2_env=env,
178 jinja2_env=env,
179 )
179 )
180
180
181 # allow custom overrides for the tornado web app.
181 # allow custom overrides for the tornado web app.
182 settings.update(settings_overrides)
182 settings.update(settings_overrides)
183 return settings
183 return settings
184
184
185 def init_handlers(self, settings):
185 def init_handlers(self, settings):
186 # Load the (URL pattern, handler) tuples for each component.
186 # Load the (URL pattern, handler) tuples for each component.
187 handlers = []
187 handlers = []
188 handlers.extend(load_handlers('base.handlers'))
188 handlers.extend(load_handlers('base.handlers'))
189 handlers.extend(load_handlers('tree.handlers'))
189 handlers.extend(load_handlers('tree.handlers'))
190 handlers.extend(load_handlers('auth.login'))
190 handlers.extend(load_handlers('auth.login'))
191 handlers.extend(load_handlers('auth.logout'))
191 handlers.extend(load_handlers('auth.logout'))
192 handlers.extend(load_handlers('notebook.handlers'))
192 handlers.extend(load_handlers('notebook.handlers'))
193 handlers.extend(load_handlers('nbconvert.handlers'))
193 handlers.extend(load_handlers('nbconvert.handlers'))
194 handlers.extend(load_handlers('kernelspecs.handlers'))
194 handlers.extend(load_handlers('kernelspecs.handlers'))
195 handlers.extend(load_handlers('services.kernels.handlers'))
195 handlers.extend(load_handlers('services.kernels.handlers'))
196 handlers.extend(load_handlers('services.notebooks.handlers'))
196 handlers.extend(load_handlers('services.contents.handlers'))
197 handlers.extend(load_handlers('services.clusters.handlers'))
197 handlers.extend(load_handlers('services.clusters.handlers'))
198 handlers.extend(load_handlers('services.sessions.handlers'))
198 handlers.extend(load_handlers('services.sessions.handlers'))
199 handlers.extend(load_handlers('services.nbconvert.handlers'))
199 handlers.extend(load_handlers('services.nbconvert.handlers'))
200 handlers.extend(load_handlers('services.kernelspecs.handlers'))
200 handlers.extend(load_handlers('services.kernelspecs.handlers'))
201 # FIXME: /files/ should be handled by the Contents service when it exists
201 # FIXME: /files/ should be handled by the Contents service when it exists
202 nbm = settings['notebook_manager']
202 cm = settings['contents_manager']
203 if hasattr(nbm, 'notebook_dir'):
203 if hasattr(cm, 'root_dir'):
204 handlers.extend([
204 handlers.append(
205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}),
206 )
207 handlers.append(
206 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
207 ])
209 )
208 # prepend base_url onto the patterns that we match
210 # prepend base_url onto the patterns that we match
209 new_handlers = []
211 new_handlers = []
210 for handler in handlers:
212 for handler in handlers:
211 pattern = url_path_join(settings['base_url'], handler[0])
213 pattern = url_path_join(settings['base_url'], handler[0])
212 new_handler = tuple([pattern] + list(handler[1:]))
214 new_handler = tuple([pattern] + list(handler[1:]))
213 new_handlers.append(new_handler)
215 new_handlers.append(new_handler)
214 # add 404 on the end, which will catch everything that falls through
216 # add 404 on the end, which will catch everything that falls through
215 new_handlers.append((r'(.*)', Template404))
217 new_handlers.append((r'(.*)', Template404))
216 return new_handlers
218 return new_handlers
217
219
218
220
219 class NbserverListApp(BaseIPythonApplication):
221 class NbserverListApp(BaseIPythonApplication):
220
222
221 description="List currently running notebook servers in this profile."
223 description="List currently running notebook servers in this profile."
222
224
223 flags = dict(
225 flags = dict(
224 json=({'NbserverListApp': {'json': True}},
226 json=({'NbserverListApp': {'json': True}},
225 "Produce machine-readable JSON output."),
227 "Produce machine-readable JSON output."),
226 )
228 )
227
229
228 json = Bool(False, config=True,
230 json = Bool(False, config=True,
229 help="If True, each line of output will be a JSON object with the "
231 help="If True, each line of output will be a JSON object with the "
230 "details from the server info file.")
232 "details from the server info file.")
231
233
232 def start(self):
234 def start(self):
233 if not self.json:
235 if not self.json:
234 print("Currently running servers:")
236 print("Currently running servers:")
235 for serverinfo in list_running_servers(self.profile):
237 for serverinfo in list_running_servers(self.profile):
236 if self.json:
238 if self.json:
237 print(json.dumps(serverinfo))
239 print(json.dumps(serverinfo))
238 else:
240 else:
239 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
241 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
240
242
241 #-----------------------------------------------------------------------------
243 #-----------------------------------------------------------------------------
242 # Aliases and Flags
244 # Aliases and Flags
243 #-----------------------------------------------------------------------------
245 #-----------------------------------------------------------------------------
244
246
245 flags = dict(base_flags)
247 flags = dict(base_flags)
246 flags['no-browser']=(
248 flags['no-browser']=(
247 {'NotebookApp' : {'open_browser' : False}},
249 {'NotebookApp' : {'open_browser' : False}},
248 "Don't open the notebook in a browser after startup."
250 "Don't open the notebook in a browser after startup."
249 )
251 )
250 flags['pylab']=(
252 flags['pylab']=(
251 {'NotebookApp' : {'pylab' : 'warn'}},
253 {'NotebookApp' : {'pylab' : 'warn'}},
252 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
254 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
253 )
255 )
254 flags['no-mathjax']=(
256 flags['no-mathjax']=(
255 {'NotebookApp' : {'enable_mathjax' : False}},
257 {'NotebookApp' : {'enable_mathjax' : False}},
256 """Disable MathJax
258 """Disable MathJax
257
259
258 MathJax is the javascript library IPython uses to render math/LaTeX. It is
260 MathJax is the javascript library IPython uses to render math/LaTeX. It is
259 very large, so you may want to disable it if you have a slow internet
261 very large, so you may want to disable it if you have a slow internet
260 connection, or for offline use of the notebook.
262 connection, or for offline use of the notebook.
261
263
262 When disabled, equations etc. will appear as their untransformed TeX source.
264 When disabled, equations etc. will appear as their untransformed TeX source.
263 """
265 """
264 )
266 )
265
267
266 # Add notebook manager flags
267 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
268 'Auto-save a .py script everytime the .ipynb notebook is saved',
269 'Do not auto-save .py scripts for every notebook'))
270
271 aliases = dict(base_aliases)
268 aliases = dict(base_aliases)
272
269
273 aliases.update({
270 aliases.update({
274 'ip': 'NotebookApp.ip',
271 'ip': 'NotebookApp.ip',
275 'port': 'NotebookApp.port',
272 'port': 'NotebookApp.port',
276 'port-retries': 'NotebookApp.port_retries',
273 'port-retries': 'NotebookApp.port_retries',
277 'transport': 'KernelManager.transport',
274 'transport': 'KernelManager.transport',
278 'keyfile': 'NotebookApp.keyfile',
275 'keyfile': 'NotebookApp.keyfile',
279 'certfile': 'NotebookApp.certfile',
276 'certfile': 'NotebookApp.certfile',
280 'notebook-dir': 'NotebookApp.notebook_dir',
277 'notebook-dir': 'NotebookApp.notebook_dir',
281 'browser': 'NotebookApp.browser',
278 'browser': 'NotebookApp.browser',
282 'pylab': 'NotebookApp.pylab',
279 'pylab': 'NotebookApp.pylab',
283 })
280 })
284
281
285 #-----------------------------------------------------------------------------
282 #-----------------------------------------------------------------------------
286 # NotebookApp
283 # NotebookApp
287 #-----------------------------------------------------------------------------
284 #-----------------------------------------------------------------------------
288
285
289 class NotebookApp(BaseIPythonApplication):
286 class NotebookApp(BaseIPythonApplication):
290
287
291 name = 'ipython-notebook'
288 name = 'ipython-notebook'
292
289
293 description = """
290 description = """
294 The IPython HTML Notebook.
291 The IPython HTML Notebook.
295
292
296 This launches a Tornado based HTML Notebook Server that serves up an
293 This launches a Tornado based HTML Notebook Server that serves up an
297 HTML5/Javascript Notebook client.
294 HTML5/Javascript Notebook client.
298 """
295 """
299 examples = _examples
296 examples = _examples
300 aliases = aliases
297 aliases = aliases
301 flags = flags
298 flags = flags
302
299
303 classes = [
300 classes = [
304 KernelManager, ProfileDir, Session, MappingKernelManager,
301 KernelManager, ProfileDir, Session, MappingKernelManager,
305 NotebookManager, FileNotebookManager, NotebookNotary,
302 ContentsManager, FileContentsManager, NotebookNotary,
306 ]
303 ]
307 flags = Dict(flags)
304 flags = Dict(flags)
308 aliases = Dict(aliases)
305 aliases = Dict(aliases)
309
306
310 subcommands = dict(
307 subcommands = dict(
311 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
308 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
312 )
309 )
313
310
314 kernel_argv = List(Unicode)
311 kernel_argv = List(Unicode)
315
312
316 _log_formatter_cls = LogFormatter
313 _log_formatter_cls = LogFormatter
317
314
318 def _log_level_default(self):
315 def _log_level_default(self):
319 return logging.INFO
316 return logging.INFO
320
317
321 def _log_datefmt_default(self):
318 def _log_datefmt_default(self):
322 """Exclude date from default date format"""
319 """Exclude date from default date format"""
323 return "%H:%M:%S"
320 return "%H:%M:%S"
324
321
325 def _log_format_default(self):
322 def _log_format_default(self):
326 """override default log format to include time"""
323 """override default log format to include time"""
327 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
324 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
328
325
329 # create requested profiles by default, if they don't exist:
326 # create requested profiles by default, if they don't exist:
330 auto_create = Bool(True)
327 auto_create = Bool(True)
331
328
332 # file to be opened in the notebook server
329 # file to be opened in the notebook server
333 file_to_run = Unicode('', config=True)
330 file_to_run = Unicode('', config=True)
334 def _file_to_run_changed(self, name, old, new):
331 def _file_to_run_changed(self, name, old, new):
335 path, base = os.path.split(new)
332 path, base = os.path.split(new)
336 if path:
333 if path:
337 self.file_to_run = base
334 self.file_to_run = base
338 self.notebook_dir = path
335 self.notebook_dir = path
339
336
340 # Network related information
337 # Network related information
341
338
342 allow_origin = Unicode('', config=True,
339 allow_origin = Unicode('', config=True,
343 help="""Set the Access-Control-Allow-Origin header
340 help="""Set the Access-Control-Allow-Origin header
344
341
345 Use '*' to allow any origin to access your server.
342 Use '*' to allow any origin to access your server.
346
343
347 Takes precedence over allow_origin_pat.
344 Takes precedence over allow_origin_pat.
348 """
345 """
349 )
346 )
350
347
351 allow_origin_pat = Unicode('', config=True,
348 allow_origin_pat = Unicode('', config=True,
352 help="""Use a regular expression for the Access-Control-Allow-Origin header
349 help="""Use a regular expression for the Access-Control-Allow-Origin header
353
350
354 Requests from an origin matching the expression will get replies with:
351 Requests from an origin matching the expression will get replies with:
355
352
356 Access-Control-Allow-Origin: origin
353 Access-Control-Allow-Origin: origin
357
354
358 where `origin` is the origin of the request.
355 where `origin` is the origin of the request.
359
356
360 Ignored if allow_origin is set.
357 Ignored if allow_origin is set.
361 """
358 """
362 )
359 )
363
360
364 allow_credentials = Bool(False, config=True,
361 allow_credentials = Bool(False, config=True,
365 help="Set the Access-Control-Allow-Credentials: true header"
362 help="Set the Access-Control-Allow-Credentials: true header"
366 )
363 )
367
364
368 ip = Unicode('localhost', config=True,
365 ip = Unicode('localhost', config=True,
369 help="The IP address the notebook server will listen on."
366 help="The IP address the notebook server will listen on."
370 )
367 )
371
368
372 def _ip_changed(self, name, old, new):
369 def _ip_changed(self, name, old, new):
373 if new == u'*': self.ip = u''
370 if new == u'*': self.ip = u''
374
371
375 port = Integer(8888, config=True,
372 port = Integer(8888, config=True,
376 help="The port the notebook server will listen on."
373 help="The port the notebook server will listen on."
377 )
374 )
378 port_retries = Integer(50, config=True,
375 port_retries = Integer(50, config=True,
379 help="The number of additional ports to try if the specified port is not available."
376 help="The number of additional ports to try if the specified port is not available."
380 )
377 )
381
378
382 certfile = Unicode(u'', config=True,
379 certfile = Unicode(u'', config=True,
383 help="""The full path to an SSL/TLS certificate file."""
380 help="""The full path to an SSL/TLS certificate file."""
384 )
381 )
385
382
386 keyfile = Unicode(u'', config=True,
383 keyfile = Unicode(u'', config=True,
387 help="""The full path to a private key file for usage with SSL/TLS."""
384 help="""The full path to a private key file for usage with SSL/TLS."""
388 )
385 )
389
386
390 cookie_secret_file = Unicode(config=True,
387 cookie_secret_file = Unicode(config=True,
391 help="""The file where the cookie secret is stored."""
388 help="""The file where the cookie secret is stored."""
392 )
389 )
393 def _cookie_secret_file_default(self):
390 def _cookie_secret_file_default(self):
394 if self.profile_dir is None:
391 if self.profile_dir is None:
395 return ''
392 return ''
396 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
393 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
397
394
398 cookie_secret = Bytes(b'', config=True,
395 cookie_secret = Bytes(b'', config=True,
399 help="""The random bytes used to secure cookies.
396 help="""The random bytes used to secure cookies.
400 By default this is a new random number every time you start the Notebook.
397 By default this is a new random number every time you start the Notebook.
401 Set it to a value in a config file to enable logins to persist across server sessions.
398 Set it to a value in a config file to enable logins to persist across server sessions.
402
399
403 Note: Cookie secrets should be kept private, do not share config files with
400 Note: Cookie secrets should be kept private, do not share config files with
404 cookie_secret stored in plaintext (you can read the value from a file).
401 cookie_secret stored in plaintext (you can read the value from a file).
405 """
402 """
406 )
403 )
407 def _cookie_secret_default(self):
404 def _cookie_secret_default(self):
408 if os.path.exists(self.cookie_secret_file):
405 if os.path.exists(self.cookie_secret_file):
409 with io.open(self.cookie_secret_file, 'rb') as f:
406 with io.open(self.cookie_secret_file, 'rb') as f:
410 return f.read()
407 return f.read()
411 else:
408 else:
412 secret = base64.encodestring(os.urandom(1024))
409 secret = base64.encodestring(os.urandom(1024))
413 self._write_cookie_secret_file(secret)
410 self._write_cookie_secret_file(secret)
414 return secret
411 return secret
415
412
416 def _write_cookie_secret_file(self, secret):
413 def _write_cookie_secret_file(self, secret):
417 """write my secret to my secret_file"""
414 """write my secret to my secret_file"""
418 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
415 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
419 with io.open(self.cookie_secret_file, 'wb') as f:
416 with io.open(self.cookie_secret_file, 'wb') as f:
420 f.write(secret)
417 f.write(secret)
421 try:
418 try:
422 os.chmod(self.cookie_secret_file, 0o600)
419 os.chmod(self.cookie_secret_file, 0o600)
423 except OSError:
420 except OSError:
424 self.log.warn(
421 self.log.warn(
425 "Could not set permissions on %s",
422 "Could not set permissions on %s",
426 self.cookie_secret_file
423 self.cookie_secret_file
427 )
424 )
428
425
429 password = Unicode(u'', config=True,
426 password = Unicode(u'', config=True,
430 help="""Hashed password to use for web authentication.
427 help="""Hashed password to use for web authentication.
431
428
432 To generate, type in a python/IPython shell:
429 To generate, type in a python/IPython shell:
433
430
434 from IPython.lib import passwd; passwd()
431 from IPython.lib import passwd; passwd()
435
432
436 The string should be of the form type:salt:hashed-password.
433 The string should be of the form type:salt:hashed-password.
437 """
434 """
438 )
435 )
439
436
440 open_browser = Bool(True, config=True,
437 open_browser = Bool(True, config=True,
441 help="""Whether to open in a browser after starting.
438 help="""Whether to open in a browser after starting.
442 The specific browser used is platform dependent and
439 The specific browser used is platform dependent and
443 determined by the python standard library `webbrowser`
440 determined by the python standard library `webbrowser`
444 module, unless it is overridden using the --browser
441 module, unless it is overridden using the --browser
445 (NotebookApp.browser) configuration option.
442 (NotebookApp.browser) configuration option.
446 """)
443 """)
447
444
448 browser = Unicode(u'', config=True,
445 browser = Unicode(u'', config=True,
449 help="""Specify what command to use to invoke a web
446 help="""Specify what command to use to invoke a web
450 browser when opening the notebook. If not specified, the
447 browser when opening the notebook. If not specified, the
451 default browser will be determined by the `webbrowser`
448 default browser will be determined by the `webbrowser`
452 standard library module, which allows setting of the
449 standard library module, which allows setting of the
453 BROWSER environment variable to override it.
450 BROWSER environment variable to override it.
454 """)
451 """)
455
452
456 webapp_settings = Dict(config=True,
453 webapp_settings = Dict(config=True,
457 help="Supply overrides for the tornado.web.Application that the "
454 help="Supply overrides for the tornado.web.Application that the "
458 "IPython notebook uses.")
455 "IPython notebook uses.")
459
456
460 jinja_environment_options = Dict(config=True,
457 jinja_environment_options = Dict(config=True,
461 help="Supply extra arguments that will be passed to Jinja environment.")
458 help="Supply extra arguments that will be passed to Jinja environment.")
462
459
463
460
464 enable_mathjax = Bool(True, config=True,
461 enable_mathjax = Bool(True, config=True,
465 help="""Whether to enable MathJax for typesetting math/TeX
462 help="""Whether to enable MathJax for typesetting math/TeX
466
463
467 MathJax is the javascript library IPython uses to render math/LaTeX. It is
464 MathJax is the javascript library IPython uses to render math/LaTeX. It is
468 very large, so you may want to disable it if you have a slow internet
465 very large, so you may want to disable it if you have a slow internet
469 connection, or for offline use of the notebook.
466 connection, or for offline use of the notebook.
470
467
471 When disabled, equations etc. will appear as their untransformed TeX source.
468 When disabled, equations etc. will appear as their untransformed TeX source.
472 """
469 """
473 )
470 )
474 def _enable_mathjax_changed(self, name, old, new):
471 def _enable_mathjax_changed(self, name, old, new):
475 """set mathjax url to empty if mathjax is disabled"""
472 """set mathjax url to empty if mathjax is disabled"""
476 if not new:
473 if not new:
477 self.mathjax_url = u''
474 self.mathjax_url = u''
478
475
479 base_url = Unicode('/', config=True,
476 base_url = Unicode('/', config=True,
480 help='''The base URL for the notebook server.
477 help='''The base URL for the notebook server.
481
478
482 Leading and trailing slashes can be omitted,
479 Leading and trailing slashes can be omitted,
483 and will automatically be added.
480 and will automatically be added.
484 ''')
481 ''')
485 def _base_url_changed(self, name, old, new):
482 def _base_url_changed(self, name, old, new):
486 if not new.startswith('/'):
483 if not new.startswith('/'):
487 self.base_url = '/'+new
484 self.base_url = '/'+new
488 elif not new.endswith('/'):
485 elif not new.endswith('/'):
489 self.base_url = new+'/'
486 self.base_url = new+'/'
490
487
491 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
488 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
492 def _base_project_url_changed(self, name, old, new):
489 def _base_project_url_changed(self, name, old, new):
493 self.log.warn("base_project_url is deprecated, use base_url")
490 self.log.warn("base_project_url is deprecated, use base_url")
494 self.base_url = new
491 self.base_url = new
495
492
496 extra_static_paths = List(Unicode, config=True,
493 extra_static_paths = List(Unicode, config=True,
497 help="""Extra paths to search for serving static files.
494 help="""Extra paths to search for serving static files.
498
495
499 This allows adding javascript/css to be available from the notebook server machine,
496 This allows adding javascript/css to be available from the notebook server machine,
500 or overriding individual files in the IPython"""
497 or overriding individual files in the IPython"""
501 )
498 )
502 def _extra_static_paths_default(self):
499 def _extra_static_paths_default(self):
503 return [os.path.join(self.profile_dir.location, 'static')]
500 return [os.path.join(self.profile_dir.location, 'static')]
504
501
505 @property
502 @property
506 def static_file_path(self):
503 def static_file_path(self):
507 """return extra paths + the default location"""
504 """return extra paths + the default location"""
508 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
505 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
509
506
510 nbextensions_path = List(Unicode, config=True,
507 nbextensions_path = List(Unicode, config=True,
511 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
508 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
512 )
509 )
513 def _nbextensions_path_default(self):
510 def _nbextensions_path_default(self):
514 return [os.path.join(get_ipython_dir(), 'nbextensions')]
511 return [os.path.join(get_ipython_dir(), 'nbextensions')]
515
512
516 websocket_url = Unicode("", config=True,
513 websocket_url = Unicode("", config=True,
517 help="""The base URL for websockets,
514 help="""The base URL for websockets,
518 if it differs from the HTTP server (hint: it almost certainly doesn't).
515 if it differs from the HTTP server (hint: it almost certainly doesn't).
519
516
520 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
517 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
521 """
518 """
522 )
519 )
523 mathjax_url = Unicode("", config=True,
520 mathjax_url = Unicode("", config=True,
524 help="""The url for MathJax.js."""
521 help="""The url for MathJax.js."""
525 )
522 )
526 def _mathjax_url_default(self):
523 def _mathjax_url_default(self):
527 if not self.enable_mathjax:
524 if not self.enable_mathjax:
528 return u''
525 return u''
529 static_url_prefix = self.webapp_settings.get("static_url_prefix",
526 static_url_prefix = self.webapp_settings.get("static_url_prefix",
530 url_path_join(self.base_url, "static")
527 url_path_join(self.base_url, "static")
531 )
528 )
532
529
533 # try local mathjax, either in nbextensions/mathjax or static/mathjax
530 # try local mathjax, either in nbextensions/mathjax or static/mathjax
534 for (url_prefix, search_path) in [
531 for (url_prefix, search_path) in [
535 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
532 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
536 (static_url_prefix, self.static_file_path),
533 (static_url_prefix, self.static_file_path),
537 ]:
534 ]:
538 self.log.debug("searching for local mathjax in %s", search_path)
535 self.log.debug("searching for local mathjax in %s", search_path)
539 try:
536 try:
540 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
537 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
541 except IOError:
538 except IOError:
542 continue
539 continue
543 else:
540 else:
544 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
541 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
545 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
542 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
546 return url
543 return url
547
544
548 # no local mathjax, serve from CDN
545 # no local mathjax, serve from CDN
549 url = u"//cdn.mathjax.org/mathjax/latest/MathJax.js"
546 url = u"//cdn.mathjax.org/mathjax/latest/MathJax.js"
550 self.log.info("Using MathJax from CDN: %s", url)
547 self.log.info("Using MathJax from CDN: %s", url)
551 return url
548 return url
552
549
553 def _mathjax_url_changed(self, name, old, new):
550 def _mathjax_url_changed(self, name, old, new):
554 if new and not self.enable_mathjax:
551 if new and not self.enable_mathjax:
555 # enable_mathjax=False overrides mathjax_url
552 # enable_mathjax=False overrides mathjax_url
556 self.mathjax_url = u''
553 self.mathjax_url = u''
557 else:
554 else:
558 self.log.info("Using MathJax: %s", new)
555 self.log.info("Using MathJax: %s", new)
559
556
560 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
557 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
561 config=True,
558 config=True,
562 help='The notebook manager class to use.'
559 help='The notebook manager class to use.'
563 )
560 )
564 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
561 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
565 config=True,
562 config=True,
566 help='The kernel manager class to use.'
563 help='The kernel manager class to use.'
567 )
564 )
568 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
565 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
569 config=True,
566 config=True,
570 help='The session manager class to use.'
567 help='The session manager class to use.'
571 )
568 )
572 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
569 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
573 config=True,
570 config=True,
574 help='The cluster manager class to use.'
571 help='The cluster manager class to use.'
575 )
572 )
576
573
577 kernel_spec_manager = Instance(KernelSpecManager)
574 kernel_spec_manager = Instance(KernelSpecManager)
578
575
579 def _kernel_spec_manager_default(self):
576 def _kernel_spec_manager_default(self):
580 return KernelSpecManager(ipython_dir=self.ipython_dir)
577 return KernelSpecManager(ipython_dir=self.ipython_dir)
581
578
582 trust_xheaders = Bool(False, config=True,
579 trust_xheaders = Bool(False, config=True,
583 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
580 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
584 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
581 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
585 )
582 )
586
583
587 info_file = Unicode()
584 info_file = Unicode()
588
585
589 def _info_file_default(self):
586 def _info_file_default(self):
590 info_file = "nbserver-%s.json"%os.getpid()
587 info_file = "nbserver-%s.json"%os.getpid()
591 return os.path.join(self.profile_dir.security_dir, info_file)
588 return os.path.join(self.profile_dir.security_dir, info_file)
592
589
593 notebook_dir = Unicode(py3compat.getcwd(), config=True,
590 notebook_dir = Unicode(py3compat.getcwd(), config=True,
594 help="The directory to use for notebooks and kernels."
591 help="The directory to use for notebooks and kernels."
595 )
592 )
596
593
597 pylab = Unicode('disabled', config=True,
594 pylab = Unicode('disabled', config=True,
598 help="""
595 help="""
599 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
596 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
600 """
597 """
601 )
598 )
602 def _pylab_changed(self, name, old, new):
599 def _pylab_changed(self, name, old, new):
603 """when --pylab is specified, display a warning and exit"""
600 """when --pylab is specified, display a warning and exit"""
604 if new != 'warn':
601 if new != 'warn':
605 backend = ' %s' % new
602 backend = ' %s' % new
606 else:
603 else:
607 backend = ''
604 backend = ''
608 self.log.error("Support for specifying --pylab on the command line has been removed.")
605 self.log.error("Support for specifying --pylab on the command line has been removed.")
609 self.log.error(
606 self.log.error(
610 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
607 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
611 )
608 )
612 self.exit(1)
609 self.exit(1)
613
610
614 def _notebook_dir_changed(self, name, old, new):
611 def _notebook_dir_changed(self, name, old, new):
615 """Do a bit of validation of the notebook dir."""
612 """Do a bit of validation of the notebook dir."""
616 if not os.path.isabs(new):
613 if not os.path.isabs(new):
617 # If we receive a non-absolute path, make it absolute.
614 # If we receive a non-absolute path, make it absolute.
618 self.notebook_dir = os.path.abspath(new)
615 self.notebook_dir = os.path.abspath(new)
619 return
616 return
620 if not os.path.isdir(new):
617 if not os.path.isdir(new):
621 raise TraitError("No such notebook dir: %r" % new)
618 raise TraitError("No such notebook dir: %r" % new)
622
619
623 # setting App.notebook_dir implies setting notebook and kernel dirs as well
620 # setting App.notebook_dir implies setting notebook and kernel dirs as well
624 self.config.FileNotebookManager.notebook_dir = new
621 self.config.FileContentsManager.root_dir = new
625 self.config.MappingKernelManager.root_dir = new
622 self.config.MappingKernelManager.root_dir = new
626
623
627
624
628 def parse_command_line(self, argv=None):
625 def parse_command_line(self, argv=None):
629 super(NotebookApp, self).parse_command_line(argv)
626 super(NotebookApp, self).parse_command_line(argv)
630
627
631 if self.extra_args:
628 if self.extra_args:
632 arg0 = self.extra_args[0]
629 arg0 = self.extra_args[0]
633 f = os.path.abspath(arg0)
630 f = os.path.abspath(arg0)
634 self.argv.remove(arg0)
631 self.argv.remove(arg0)
635 if not os.path.exists(f):
632 if not os.path.exists(f):
636 self.log.critical("No such file or directory: %s", f)
633 self.log.critical("No such file or directory: %s", f)
637 self.exit(1)
634 self.exit(1)
638
635
639 # Use config here, to ensure that it takes higher priority than
636 # Use config here, to ensure that it takes higher priority than
640 # anything that comes from the profile.
637 # anything that comes from the profile.
641 c = Config()
638 c = Config()
642 if os.path.isdir(f):
639 if os.path.isdir(f):
643 c.NotebookApp.notebook_dir = f
640 c.NotebookApp.notebook_dir = f
644 elif os.path.isfile(f):
641 elif os.path.isfile(f):
645 c.NotebookApp.file_to_run = f
642 c.NotebookApp.file_to_run = f
646 self.update_config(c)
643 self.update_config(c)
647
644
648 def init_kernel_argv(self):
645 def init_kernel_argv(self):
649 """construct the kernel arguments"""
646 """construct the kernel arguments"""
650 # Kernel should get *absolute* path to profile directory
647 # Kernel should get *absolute* path to profile directory
651 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
648 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
652
649
653 def init_configurables(self):
650 def init_configurables(self):
654 # force Session default to be secure
651 # force Session default to be secure
655 default_secure(self.config)
652 default_secure(self.config)
656 kls = import_item(self.kernel_manager_class)
653 kls = import_item(self.kernel_manager_class)
657 self.kernel_manager = kls(
654 self.kernel_manager = kls(
658 parent=self, log=self.log, kernel_argv=self.kernel_argv,
655 parent=self, log=self.log, kernel_argv=self.kernel_argv,
659 connection_dir = self.profile_dir.security_dir,
656 connection_dir = self.profile_dir.security_dir,
660 )
657 )
661 kls = import_item(self.notebook_manager_class)
658 kls = import_item(self.contents_manager_class)
662 self.notebook_manager = kls(parent=self, log=self.log)
659 self.contents_manager = kls(parent=self, log=self.log)
663 kls = import_item(self.session_manager_class)
660 kls = import_item(self.session_manager_class)
664 self.session_manager = kls(parent=self, log=self.log,
661 self.session_manager = kls(parent=self, log=self.log,
665 kernel_manager=self.kernel_manager,
662 kernel_manager=self.kernel_manager,
666 notebook_manager=self.notebook_manager)
663 contents_manager=self.contents_manager)
667 kls = import_item(self.cluster_manager_class)
664 kls = import_item(self.cluster_manager_class)
668 self.cluster_manager = kls(parent=self, log=self.log)
665 self.cluster_manager = kls(parent=self, log=self.log)
669 self.cluster_manager.update_profiles()
666 self.cluster_manager.update_profiles()
670
667
671 def init_logging(self):
668 def init_logging(self):
672 # This prevents double log messages because tornado use a root logger that
669 # This prevents double log messages because tornado use a root logger that
673 # self.log is a child of. The logging module dipatches log messages to a log
670 # self.log is a child of. The logging module dipatches log messages to a log
674 # and all of its ancenstors until propagate is set to False.
671 # and all of its ancenstors until propagate is set to False.
675 self.log.propagate = False
672 self.log.propagate = False
676
673
677 # hook up tornado 3's loggers to our app handlers
674 # hook up tornado 3's loggers to our app handlers
678 logger = logging.getLogger('tornado')
675 logger = logging.getLogger('tornado')
679 logger.propagate = True
676 logger.propagate = True
680 logger.parent = self.log
677 logger.parent = self.log
681 logger.setLevel(self.log.level)
678 logger.setLevel(self.log.level)
682
679
683 def init_webapp(self):
680 def init_webapp(self):
684 """initialize tornado webapp and httpserver"""
681 """initialize tornado webapp and httpserver"""
685 self.webapp_settings['allow_origin'] = self.allow_origin
682 self.webapp_settings['allow_origin'] = self.allow_origin
686 if self.allow_origin_pat:
683 if self.allow_origin_pat:
687 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
684 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
688 self.webapp_settings['allow_credentials'] = self.allow_credentials
685 self.webapp_settings['allow_credentials'] = self.allow_credentials
689
686
690 self.web_app = NotebookWebApplication(
687 self.web_app = NotebookWebApplication(
691 self, self.kernel_manager, self.notebook_manager,
688 self, self.kernel_manager, self.contents_manager,
692 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
689 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
693 self.log, self.base_url, self.webapp_settings,
690 self.log, self.base_url, self.webapp_settings,
694 self.jinja_environment_options
691 self.jinja_environment_options
695 )
692 )
696 if self.certfile:
693 if self.certfile:
697 ssl_options = dict(certfile=self.certfile)
694 ssl_options = dict(certfile=self.certfile)
698 if self.keyfile:
695 if self.keyfile:
699 ssl_options['keyfile'] = self.keyfile
696 ssl_options['keyfile'] = self.keyfile
700 else:
697 else:
701 ssl_options = None
698 ssl_options = None
702 self.web_app.password = self.password
699 self.web_app.password = self.password
703 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
700 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
704 xheaders=self.trust_xheaders)
701 xheaders=self.trust_xheaders)
705 if not self.ip:
702 if not self.ip:
706 warning = "WARNING: The notebook server is listening on all IP addresses"
703 warning = "WARNING: The notebook server is listening on all IP addresses"
707 if ssl_options is None:
704 if ssl_options is None:
708 self.log.critical(warning + " and not using encryption. This "
705 self.log.critical(warning + " and not using encryption. This "
709 "is not recommended.")
706 "is not recommended.")
710 if not self.password:
707 if not self.password:
711 self.log.critical(warning + " and not using authentication. "
708 self.log.critical(warning + " and not using authentication. "
712 "This is highly insecure and not recommended.")
709 "This is highly insecure and not recommended.")
713 success = None
710 success = None
714 for port in random_ports(self.port, self.port_retries+1):
711 for port in random_ports(self.port, self.port_retries+1):
715 try:
712 try:
716 self.http_server.listen(port, self.ip)
713 self.http_server.listen(port, self.ip)
717 except socket.error as e:
714 except socket.error as e:
718 if e.errno == errno.EADDRINUSE:
715 if e.errno == errno.EADDRINUSE:
719 self.log.info('The port %i is already in use, trying another random port.' % port)
716 self.log.info('The port %i is already in use, trying another random port.' % port)
720 continue
717 continue
721 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
718 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
722 self.log.warn("Permission to listen on port %i denied" % port)
719 self.log.warn("Permission to listen on port %i denied" % port)
723 continue
720 continue
724 else:
721 else:
725 raise
722 raise
726 else:
723 else:
727 self.port = port
724 self.port = port
728 success = True
725 success = True
729 break
726 break
730 if not success:
727 if not success:
731 self.log.critical('ERROR: the notebook server could not be started because '
728 self.log.critical('ERROR: the notebook server could not be started because '
732 'no available port could be found.')
729 'no available port could be found.')
733 self.exit(1)
730 self.exit(1)
734
731
735 @property
732 @property
736 def display_url(self):
733 def display_url(self):
737 ip = self.ip if self.ip else '[all ip addresses on your system]'
734 ip = self.ip if self.ip else '[all ip addresses on your system]'
738 return self._url(ip)
735 return self._url(ip)
739
736
740 @property
737 @property
741 def connection_url(self):
738 def connection_url(self):
742 ip = self.ip if self.ip else 'localhost'
739 ip = self.ip if self.ip else 'localhost'
743 return self._url(ip)
740 return self._url(ip)
744
741
745 def _url(self, ip):
742 def _url(self, ip):
746 proto = 'https' if self.certfile else 'http'
743 proto = 'https' if self.certfile else 'http'
747 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
744 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
748
745
749 def init_signal(self):
746 def init_signal(self):
750 if not sys.platform.startswith('win'):
747 if not sys.platform.startswith('win'):
751 signal.signal(signal.SIGINT, self._handle_sigint)
748 signal.signal(signal.SIGINT, self._handle_sigint)
752 signal.signal(signal.SIGTERM, self._signal_stop)
749 signal.signal(signal.SIGTERM, self._signal_stop)
753 if hasattr(signal, 'SIGUSR1'):
750 if hasattr(signal, 'SIGUSR1'):
754 # Windows doesn't support SIGUSR1
751 # Windows doesn't support SIGUSR1
755 signal.signal(signal.SIGUSR1, self._signal_info)
752 signal.signal(signal.SIGUSR1, self._signal_info)
756 if hasattr(signal, 'SIGINFO'):
753 if hasattr(signal, 'SIGINFO'):
757 # only on BSD-based systems
754 # only on BSD-based systems
758 signal.signal(signal.SIGINFO, self._signal_info)
755 signal.signal(signal.SIGINFO, self._signal_info)
759
756
760 def _handle_sigint(self, sig, frame):
757 def _handle_sigint(self, sig, frame):
761 """SIGINT handler spawns confirmation dialog"""
758 """SIGINT handler spawns confirmation dialog"""
762 # register more forceful signal handler for ^C^C case
759 # register more forceful signal handler for ^C^C case
763 signal.signal(signal.SIGINT, self._signal_stop)
760 signal.signal(signal.SIGINT, self._signal_stop)
764 # request confirmation dialog in bg thread, to avoid
761 # request confirmation dialog in bg thread, to avoid
765 # blocking the App
762 # blocking the App
766 thread = threading.Thread(target=self._confirm_exit)
763 thread = threading.Thread(target=self._confirm_exit)
767 thread.daemon = True
764 thread.daemon = True
768 thread.start()
765 thread.start()
769
766
770 def _restore_sigint_handler(self):
767 def _restore_sigint_handler(self):
771 """callback for restoring original SIGINT handler"""
768 """callback for restoring original SIGINT handler"""
772 signal.signal(signal.SIGINT, self._handle_sigint)
769 signal.signal(signal.SIGINT, self._handle_sigint)
773
770
774 def _confirm_exit(self):
771 def _confirm_exit(self):
775 """confirm shutdown on ^C
772 """confirm shutdown on ^C
776
773
777 A second ^C, or answering 'y' within 5s will cause shutdown,
774 A second ^C, or answering 'y' within 5s will cause shutdown,
778 otherwise original SIGINT handler will be restored.
775 otherwise original SIGINT handler will be restored.
779
776
780 This doesn't work on Windows.
777 This doesn't work on Windows.
781 """
778 """
782 info = self.log.info
779 info = self.log.info
783 info('interrupted')
780 info('interrupted')
784 print(self.notebook_info())
781 print(self.notebook_info())
785 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
782 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
786 sys.stdout.flush()
783 sys.stdout.flush()
787 r,w,x = select.select([sys.stdin], [], [], 5)
784 r,w,x = select.select([sys.stdin], [], [], 5)
788 if r:
785 if r:
789 line = sys.stdin.readline()
786 line = sys.stdin.readline()
790 if line.lower().startswith('y') and 'n' not in line.lower():
787 if line.lower().startswith('y') and 'n' not in line.lower():
791 self.log.critical("Shutdown confirmed")
788 self.log.critical("Shutdown confirmed")
792 ioloop.IOLoop.instance().stop()
789 ioloop.IOLoop.instance().stop()
793 return
790 return
794 else:
791 else:
795 print("No answer for 5s:", end=' ')
792 print("No answer for 5s:", end=' ')
796 print("resuming operation...")
793 print("resuming operation...")
797 # no answer, or answer is no:
794 # no answer, or answer is no:
798 # set it back to original SIGINT handler
795 # set it back to original SIGINT handler
799 # use IOLoop.add_callback because signal.signal must be called
796 # use IOLoop.add_callback because signal.signal must be called
800 # from main thread
797 # from main thread
801 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
798 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
802
799
803 def _signal_stop(self, sig, frame):
800 def _signal_stop(self, sig, frame):
804 self.log.critical("received signal %s, stopping", sig)
801 self.log.critical("received signal %s, stopping", sig)
805 ioloop.IOLoop.instance().stop()
802 ioloop.IOLoop.instance().stop()
806
803
807 def _signal_info(self, sig, frame):
804 def _signal_info(self, sig, frame):
808 print(self.notebook_info())
805 print(self.notebook_info())
809
806
810 def init_components(self):
807 def init_components(self):
811 """Check the components submodule, and warn if it's unclean"""
808 """Check the components submodule, and warn if it's unclean"""
812 status = submodule.check_submodule_status()
809 status = submodule.check_submodule_status()
813 if status == 'missing':
810 if status == 'missing':
814 self.log.warn("components submodule missing, running `git submodule update`")
811 self.log.warn("components submodule missing, running `git submodule update`")
815 submodule.update_submodules(submodule.ipython_parent())
812 submodule.update_submodules(submodule.ipython_parent())
816 elif status == 'unclean':
813 elif status == 'unclean':
817 self.log.warn("components submodule unclean, you may see 404s on static/components")
814 self.log.warn("components submodule unclean, you may see 404s on static/components")
818 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
815 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
819
816
820 @catch_config_error
817 @catch_config_error
821 def initialize(self, argv=None):
818 def initialize(self, argv=None):
822 super(NotebookApp, self).initialize(argv)
819 super(NotebookApp, self).initialize(argv)
823 self.init_logging()
820 self.init_logging()
824 self.init_kernel_argv()
821 self.init_kernel_argv()
825 self.init_configurables()
822 self.init_configurables()
826 self.init_components()
823 self.init_components()
827 self.init_webapp()
824 self.init_webapp()
828 self.init_signal()
825 self.init_signal()
829
826
830 def cleanup_kernels(self):
827 def cleanup_kernels(self):
831 """Shutdown all kernels.
828 """Shutdown all kernels.
832
829
833 The kernels will shutdown themselves when this process no longer exists,
830 The kernels will shutdown themselves when this process no longer exists,
834 but explicit shutdown allows the KernelManagers to cleanup the connection files.
831 but explicit shutdown allows the KernelManagers to cleanup the connection files.
835 """
832 """
836 self.log.info('Shutting down kernels')
833 self.log.info('Shutting down kernels')
837 self.kernel_manager.shutdown_all()
834 self.kernel_manager.shutdown_all()
838
835
839 def notebook_info(self):
836 def notebook_info(self):
840 "Return the current working directory and the server url information"
837 "Return the current working directory and the server url information"
841 info = self.notebook_manager.info_string() + "\n"
838 info = self.contents_manager.info_string() + "\n"
842 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
839 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
843 return info + "The IPython Notebook is running at: %s" % self.display_url
840 return info + "The IPython Notebook is running at: %s" % self.display_url
844
841
845 def server_info(self):
842 def server_info(self):
846 """Return a JSONable dict of information about this server."""
843 """Return a JSONable dict of information about this server."""
847 return {'url': self.connection_url,
844 return {'url': self.connection_url,
848 'hostname': self.ip if self.ip else 'localhost',
845 'hostname': self.ip if self.ip else 'localhost',
849 'port': self.port,
846 'port': self.port,
850 'secure': bool(self.certfile),
847 'secure': bool(self.certfile),
851 'base_url': self.base_url,
848 'base_url': self.base_url,
852 'notebook_dir': os.path.abspath(self.notebook_dir),
849 'notebook_dir': os.path.abspath(self.notebook_dir),
853 'pid': os.getpid()
850 'pid': os.getpid()
854 }
851 }
855
852
856 def write_server_info_file(self):
853 def write_server_info_file(self):
857 """Write the result of server_info() to the JSON file info_file."""
854 """Write the result of server_info() to the JSON file info_file."""
858 with open(self.info_file, 'w') as f:
855 with open(self.info_file, 'w') as f:
859 json.dump(self.server_info(), f, indent=2)
856 json.dump(self.server_info(), f, indent=2)
860
857
861 def remove_server_info_file(self):
858 def remove_server_info_file(self):
862 """Remove the nbserver-<pid>.json file created for this server.
859 """Remove the nbserver-<pid>.json file created for this server.
863
860
864 Ignores the error raised when the file has already been removed.
861 Ignores the error raised when the file has already been removed.
865 """
862 """
866 try:
863 try:
867 os.unlink(self.info_file)
864 os.unlink(self.info_file)
868 except OSError as e:
865 except OSError as e:
869 if e.errno != errno.ENOENT:
866 if e.errno != errno.ENOENT:
870 raise
867 raise
871
868
872 def start(self):
869 def start(self):
873 """ Start the IPython Notebook server app, after initialization
870 """ Start the IPython Notebook server app, after initialization
874
871
875 This method takes no arguments so all configuration and initialization
872 This method takes no arguments so all configuration and initialization
876 must be done prior to calling this method."""
873 must be done prior to calling this method."""
877 if self.subapp is not None:
874 if self.subapp is not None:
878 return self.subapp.start()
875 return self.subapp.start()
879
876
880 info = self.log.info
877 info = self.log.info
881 for line in self.notebook_info().split("\n"):
878 for line in self.notebook_info().split("\n"):
882 info(line)
879 info(line)
883 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
880 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
884
881
885 self.write_server_info_file()
882 self.write_server_info_file()
886
883
887 if self.open_browser or self.file_to_run:
884 if self.open_browser or self.file_to_run:
888 try:
885 try:
889 browser = webbrowser.get(self.browser or None)
886 browser = webbrowser.get(self.browser or None)
890 except webbrowser.Error as e:
887 except webbrowser.Error as e:
891 self.log.warn('No web browser found: %s.' % e)
888 self.log.warn('No web browser found: %s.' % e)
892 browser = None
889 browser = None
893
890
894 if self.file_to_run:
891 if self.file_to_run:
895 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
892 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
896 if not os.path.exists(fullpath):
893 if not os.path.exists(fullpath):
897 self.log.critical("%s does not exist" % fullpath)
894 self.log.critical("%s does not exist" % fullpath)
898 self.exit(1)
895 self.exit(1)
899
896
900 uri = url_path_join('notebooks', self.file_to_run)
897 uri = url_path_join('notebooks', self.file_to_run)
901 else:
898 else:
902 uri = 'tree'
899 uri = 'tree'
903 if browser:
900 if browser:
904 b = lambda : browser.open(url_path_join(self.connection_url, uri),
901 b = lambda : browser.open(url_path_join(self.connection_url, uri),
905 new=2)
902 new=2)
906 threading.Thread(target=b).start()
903 threading.Thread(target=b).start()
907 try:
904 try:
908 ioloop.IOLoop.instance().start()
905 ioloop.IOLoop.instance().start()
909 except KeyboardInterrupt:
906 except KeyboardInterrupt:
910 info("Interrupted...")
907 info("Interrupted...")
911 finally:
908 finally:
912 self.cleanup_kernels()
909 self.cleanup_kernels()
913 self.remove_server_info_file()
910 self.remove_server_info_file()
914
911
915
912
916 def list_running_servers(profile='default'):
913 def list_running_servers(profile='default'):
917 """Iterate over the server info files of running notebook servers.
914 """Iterate over the server info files of running notebook servers.
918
915
919 Given a profile name, find nbserver-* files in the security directory of
916 Given a profile name, find nbserver-* files in the security directory of
920 that profile, and yield dicts of their information, each one pertaining to
917 that profile, and yield dicts of their information, each one pertaining to
921 a currently running notebook server instance.
918 a currently running notebook server instance.
922 """
919 """
923 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
920 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
924 for file in os.listdir(pd.security_dir):
921 for file in os.listdir(pd.security_dir):
925 if file.startswith('nbserver-'):
922 if file.startswith('nbserver-'):
926 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
923 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
927 info = json.load(f)
924 info = json.load(f)
928
925
929 # Simple check whether that process is really still running
926 # Simple check whether that process is really still running
930 if check_pid(info['pid']):
927 if check_pid(info['pid']):
931 yield info
928 yield info
932 else:
929 else:
933 # If the process has died, try to delete its info file
930 # If the process has died, try to delete its info file
934 try:
931 try:
935 os.unlink(file)
932 os.unlink(file)
936 except OSError:
933 except OSError:
937 pass # TODO: This should warn or log or something
934 pass # TODO: This should warn or log or something
938 #-----------------------------------------------------------------------------
935 #-----------------------------------------------------------------------------
939 # Main entry point
936 # Main entry point
940 #-----------------------------------------------------------------------------
937 #-----------------------------------------------------------------------------
941
938
942 launch_new_instance = NotebookApp.launch_instance
939 launch_new_instance = NotebookApp.launch_instance
943
940
@@ -1,470 +1,437 b''
1 """A notebook manager that uses the local file system for storage."""
1 """A contents manager that uses the local file system for storage."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import io
6 import io
7 import os
7 import os
8 import glob
8 import glob
9 import shutil
9 import shutil
10
10
11 from tornado import web
11 from tornado import web
12
12
13 from .nbmanager import NotebookManager
13 from .manager import ContentsManager
14 from IPython.nbformat import current
14 from IPython.nbformat import current
15 from IPython.utils.path import ensure_dir_exists
15 from IPython.utils.path import ensure_dir_exists
16 from IPython.utils.traitlets import Unicode, Bool, TraitError
16 from IPython.utils.traitlets import Unicode, Bool, TraitError
17 from IPython.utils.py3compat import getcwd
17 from IPython.utils.py3compat import getcwd
18 from IPython.utils import tz
18 from IPython.utils import tz
19 from IPython.html.utils import is_hidden, to_os_path
19 from IPython.html.utils import is_hidden, to_os_path
20
20
21 def sort_key(item):
21 def sort_key(item):
22 """Case-insensitive sorting."""
22 """Case-insensitive sorting."""
23 return item['name'].lower()
23 return item['name'].lower()
24
24
25 #-----------------------------------------------------------------------------
26 # Classes
27 #-----------------------------------------------------------------------------
28
25
29 class FileNotebookManager(NotebookManager):
26 class FileContentsManager(ContentsManager):
30
27
31 save_script = Bool(False, config=True,
28 root_dir = Unicode(getcwd(), config=True)
32 help="""Automatically create a Python script when saving the notebook.
33
29
34 For easier use of import, %run and %load across notebooks, a
30 def _root_dir_changed(self, name, old, new):
35 <notebook-name>.py script will be created next to any
31 """Do a bit of validation of the root_dir."""
36 <notebook-name>.ipynb on each save. This can also be set with the
37 short `--script` flag.
38 """
39 )
40 notebook_dir = Unicode(getcwd(), config=True)
41
42 def _notebook_dir_changed(self, name, old, new):
43 """Do a bit of validation of the notebook dir."""
44 if not os.path.isabs(new):
32 if not os.path.isabs(new):
45 # If we receive a non-absolute path, make it absolute.
33 # If we receive a non-absolute path, make it absolute.
46 self.notebook_dir = os.path.abspath(new)
34 self.root_dir = os.path.abspath(new)
47 return
35 return
48 if not os.path.exists(new) or not os.path.isdir(new):
36 if not os.path.exists(new) or not os.path.isdir(new):
49 raise TraitError("notebook dir %r is not a directory" % new)
37 raise TraitError("%r is not a directory" % new)
50
38
51 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
39 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
52 help="""The directory name in which to keep notebook checkpoints
40 help="""The directory name in which to keep notebook checkpoints
53
41
54 This is a path relative to the notebook's own directory.
42 This is a path relative to the notebook's own directory.
55
43
56 By default, it is .ipynb_checkpoints
44 By default, it is .ipynb_checkpoints
57 """
45 """
58 )
46 )
59
47
60 def _copy(self, src, dest):
48 def _copy(self, src, dest):
61 """copy src to dest
49 """copy src to dest
62
50
63 like shutil.copy2, but log errors in copystat
51 like shutil.copy2, but log errors in copystat
64 """
52 """
65 shutil.copyfile(src, dest)
53 shutil.copyfile(src, dest)
66 try:
54 try:
67 shutil.copystat(src, dest)
55 shutil.copystat(src, dest)
68 except OSError as e:
56 except OSError as e:
69 self.log.debug("copystat on %s failed", dest, exc_info=True)
57 self.log.debug("copystat on %s failed", dest, exc_info=True)
70
58
71 def get_notebook_names(self, path=''):
59 def get_names(self, path=''):
72 """List all notebook names in the notebook dir and path."""
60 """List all filenames in the path (relative to root_dir)."""
73 path = path.strip('/')
61 path = path.strip('/')
74 if not os.path.isdir(self._get_os_path(path=path)):
62 if not os.path.isdir(self._get_os_path(path=path)):
75 raise web.HTTPError(404, 'Directory not found: ' + path)
63 raise web.HTTPError(404, 'Directory not found: ' + path)
76 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
64 names = glob.glob(self._get_os_path('*', path))
77 names = [os.path.basename(name)
65 names = [ os.path.basename(name) for name in names if os.path.isfile(name)]
78 for name in names]
79 return names
66 return names
80
67
81 def path_exists(self, path):
68 def path_exists(self, path):
82 """Does the API-style path (directory) actually exist?
69 """Does the API-style path (directory) actually exist?
83
70
84 Parameters
71 Parameters
85 ----------
72 ----------
86 path : string
73 path : string
87 The path to check. This is an API path (`/` separated,
74 The path to check. This is an API path (`/` separated,
88 relative to base notebook-dir).
75 relative to root_dir).
89
76
90 Returns
77 Returns
91 -------
78 -------
92 exists : bool
79 exists : bool
93 Whether the path is indeed a directory.
80 Whether the path is indeed a directory.
94 """
81 """
95 path = path.strip('/')
82 path = path.strip('/')
96 os_path = self._get_os_path(path=path)
83 os_path = self._get_os_path(path=path)
97 return os.path.isdir(os_path)
84 return os.path.isdir(os_path)
98
85
99 def is_hidden(self, path):
86 def is_hidden(self, path):
100 """Does the API style path correspond to a hidden directory or file?
87 """Does the API style path correspond to a hidden directory or file?
101
88
102 Parameters
89 Parameters
103 ----------
90 ----------
104 path : string
91 path : string
105 The path to check. This is an API path (`/` separated,
92 The path to check. This is an API path (`/` separated,
106 relative to base notebook-dir).
93 relative to root_dir).
107
94
108 Returns
95 Returns
109 -------
96 -------
110 exists : bool
97 exists : bool
111 Whether the path is hidden.
98 Whether the path is hidden.
112
99
113 """
100 """
114 path = path.strip('/')
101 path = path.strip('/')
115 os_path = self._get_os_path(path=path)
102 os_path = self._get_os_path(path=path)
116 return is_hidden(os_path, self.notebook_dir)
103 return is_hidden(os_path, self.root_dir)
117
104
118 def _get_os_path(self, name=None, path=''):
105 def _get_os_path(self, name=None, path=''):
119 """Given a notebook name and a URL path, return its file system
106 """Given a filename and a URL path, return its file system
120 path.
107 path.
121
108
122 Parameters
109 Parameters
123 ----------
110 ----------
124 name : string
111 name : string
125 The name of a notebook file with the .ipynb extension
112 A filename
126 path : string
113 path : string
127 The relative URL path (with '/' as separator) to the named
114 The relative URL path (with '/' as separator) to the named
128 notebook.
115 file.
129
116
130 Returns
117 Returns
131 -------
118 -------
132 path : string
119 path : string
133 A file system path that combines notebook_dir (location where
120 API path to be evaluated relative to root_dir.
134 server started), the relative path, and the filename with the
135 current operating system's url.
136 """
121 """
137 if name is not None:
122 if name is not None:
138 path = path + '/' + name
123 path = path + '/' + name
139 return to_os_path(path, self.notebook_dir)
124 return to_os_path(path, self.root_dir)
140
125
141 def notebook_exists(self, name, path=''):
126 def file_exists(self, name, path=''):
142 """Returns a True if the notebook exists. Else, returns False.
127 """Returns a True if the file exists, else returns False.
143
128
144 Parameters
129 Parameters
145 ----------
130 ----------
146 name : string
131 name : string
147 The name of the notebook you are checking.
132 The name of the file you are checking.
148 path : string
133 path : string
149 The relative path to the notebook (with '/' as separator)
134 The relative path to the file's directory (with '/' as separator)
150
135
151 Returns
136 Returns
152 -------
137 -------
153 bool
138 bool
154 """
139 """
155 path = path.strip('/')
140 path = path.strip('/')
156 nbpath = self._get_os_path(name, path=path)
141 nbpath = self._get_os_path(name, path=path)
157 return os.path.isfile(nbpath)
142 return os.path.isfile(nbpath)
158
143
159 # TODO: Remove this after we create the contents web service and directories are
144 # TODO: Remove this after we create the contents web service and directories are
160 # no longer listed by the notebook web service.
145 # no longer listed by the notebook web service.
161 def list_dirs(self, path):
146 def list_dirs(self, path):
162 """List the directories for a given API style path."""
147 """List the directories for a given API style path."""
163 path = path.strip('/')
148 path = path.strip('/')
164 os_path = self._get_os_path('', path)
149 os_path = self._get_os_path('', path)
165 if not os.path.isdir(os_path):
150 if not os.path.isdir(os_path):
166 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
151 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
167 elif is_hidden(os_path, self.notebook_dir):
152 elif is_hidden(os_path, self.root_dir):
168 self.log.info("Refusing to serve hidden directory, via 404 Error")
153 self.log.info("Refusing to serve hidden directory, via 404 Error")
169 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
154 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
170 dir_names = os.listdir(os_path)
155 dir_names = os.listdir(os_path)
171 dirs = []
156 dirs = []
172 for name in dir_names:
157 for name in dir_names:
173 os_path = self._get_os_path(name, path)
158 os_path = self._get_os_path(name, path)
174 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
159 if os.path.isdir(os_path) and not is_hidden(os_path, self.root_dir)\
175 and self.should_list(name):
160 and self.should_list(name):
176 try:
161 try:
177 model = self.get_dir_model(name, path)
162 model = self.get_dir_model(name, path)
178 except IOError:
163 except IOError:
179 pass
164 pass
180 dirs.append(model)
165 dirs.append(model)
181 dirs = sorted(dirs, key=sort_key)
166 dirs = sorted(dirs, key=sort_key)
182 return dirs
167 return dirs
183
168
184 # TODO: Remove this after we create the contents web service and directories are
169 # TODO: Remove this after we create the contents web service and directories are
185 # no longer listed by the notebook web service.
170 # no longer listed by the notebook web service.
186 def get_dir_model(self, name, path=''):
171 def get_dir_model(self, name, path=''):
187 """Get the directory model given a directory name and its API style path"""
172 """Get the directory model given a directory name and its API style path"""
188 path = path.strip('/')
173 path = path.strip('/')
189 os_path = self._get_os_path(name, path)
174 os_path = self._get_os_path(name, path)
190 if not os.path.isdir(os_path):
175 if not os.path.isdir(os_path):
191 raise IOError('directory does not exist: %r' % os_path)
176 raise IOError('directory does not exist: %r' % os_path)
192 info = os.stat(os_path)
177 info = os.stat(os_path)
193 last_modified = tz.utcfromtimestamp(info.st_mtime)
178 last_modified = tz.utcfromtimestamp(info.st_mtime)
194 created = tz.utcfromtimestamp(info.st_ctime)
179 created = tz.utcfromtimestamp(info.st_ctime)
195 # Create the notebook model.
180 # Create the notebook model.
196 model ={}
181 model ={}
197 model['name'] = name
182 model['name'] = name
198 model['path'] = path
183 model['path'] = path
199 model['last_modified'] = last_modified
184 model['last_modified'] = last_modified
200 model['created'] = created
185 model['created'] = created
201 model['type'] = 'directory'
186 model['type'] = 'directory'
202 return model
187 return model
203
188
204 def list_notebooks(self, path):
189 def list_files(self, path):
205 """Returns a list of dictionaries that are the standard model
190 """Returns a list of dictionaries that are the standard model
206 for all notebooks in the relative 'path'.
191 for all notebooks in the relative 'path'.
207
192
208 Parameters
193 Parameters
209 ----------
194 ----------
210 path : str
195 path : str
211 the URL path that describes the relative path for the
196 the URL path that describes the relative path for the
212 listed notebooks
197 listed notebooks
213
198
214 Returns
199 Returns
215 -------
200 -------
216 notebooks : list of dicts
201 notebooks : list of dicts
217 a list of the notebook models without 'content'
202 a list of the notebook models without 'content'
218 """
203 """
219 path = path.strip('/')
204 path = path.strip('/')
220 notebook_names = self.get_notebook_names(path)
205 names = self.get_names(path)
221 notebooks = [self.get_notebook(name, path, content=False)
206 notebooks = [self.get(name, path, content=False)
222 for name in notebook_names if self.should_list(name)]
207 for name in names if self.should_list(name)]
223 notebooks = sorted(notebooks, key=sort_key)
208 notebooks = sorted(notebooks, key=sort_key)
224 return notebooks
209 return notebooks
225
210
226 def get_notebook(self, name, path='', content=True):
211 def get(self, name, path='', content=True):
227 """ Takes a path and name for a notebook and returns its model
212 """ Takes a path and name for a notebook and returns its model
228
213
229 Parameters
214 Parameters
230 ----------
215 ----------
231 name : str
216 name : str
232 the name of the notebook
217 the name of the notebook
233 path : str
218 path : str
234 the URL path that describes the relative path for
219 the URL path that describes the relative path for
235 the notebook
220 the notebook
236
221
237 Returns
222 Returns
238 -------
223 -------
239 model : dict
224 model : dict
240 the notebook model. If contents=True, returns the 'contents'
225 the notebook model. If contents=True, returns the 'contents'
241 dict in the model as well.
226 dict in the model as well.
242 """
227 """
243 path = path.strip('/')
228 path = path.strip('/')
244 if not self.notebook_exists(name=name, path=path):
229 if not self.file_exists(name=name, path=path):
245 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
230 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
246 os_path = self._get_os_path(name, path)
231 os_path = self._get_os_path(name, path)
247 info = os.stat(os_path)
232 info = os.stat(os_path)
248 last_modified = tz.utcfromtimestamp(info.st_mtime)
233 last_modified = tz.utcfromtimestamp(info.st_mtime)
249 created = tz.utcfromtimestamp(info.st_ctime)
234 created = tz.utcfromtimestamp(info.st_ctime)
250 # Create the notebook model.
235 # Create the notebook model.
251 model ={}
236 model ={}
252 model['name'] = name
237 model['name'] = name
253 model['path'] = path
238 model['path'] = path
254 model['last_modified'] = last_modified
239 model['last_modified'] = last_modified
255 model['created'] = created
240 model['created'] = created
256 model['type'] = 'notebook'
241 model['type'] = 'notebook'
257 if content:
242 if content:
258 with io.open(os_path, 'r', encoding='utf-8') as f:
243 with io.open(os_path, 'r', encoding='utf-8') as f:
259 try:
244 try:
260 nb = current.read(f, u'json')
245 nb = current.read(f, u'json')
261 except Exception as e:
246 except Exception as e:
262 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
247 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
263 self.mark_trusted_cells(nb, name, path)
248 self.mark_trusted_cells(nb, name, path)
264 model['content'] = nb
249 model['content'] = nb
265 return model
250 return model
266
251
267 def save_notebook(self, model, name='', path=''):
252 def save(self, model, name='', path=''):
268 """Save the notebook model and return the model with no content."""
253 """Save the notebook model and return the model with no content."""
269 path = path.strip('/')
254 path = path.strip('/')
270
255
271 if 'content' not in model:
256 if 'content' not in model:
272 raise web.HTTPError(400, u'No notebook JSON data provided')
257 raise web.HTTPError(400, u'No notebook JSON data provided')
273
258
274 # One checkpoint should always exist
259 # One checkpoint should always exist
275 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
260 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
276 self.create_checkpoint(name, path)
261 self.create_checkpoint(name, path)
277
262
278 new_path = model.get('path', path).strip('/')
263 new_path = model.get('path', path).strip('/')
279 new_name = model.get('name', name)
264 new_name = model.get('name', name)
280
265
281 if path != new_path or name != new_name:
266 if path != new_path or name != new_name:
282 self.rename_notebook(name, path, new_name, new_path)
267 self.rename(name, path, new_name, new_path)
283
268
284 # Save the notebook file
269 # Save the notebook file
285 os_path = self._get_os_path(new_name, new_path)
270 os_path = self._get_os_path(new_name, new_path)
286 nb = current.to_notebook_json(model['content'])
271 nb = current.to_notebook_json(model['content'])
287
272
288 self.check_and_sign(nb, new_name, new_path)
273 self.check_and_sign(nb, new_name, new_path)
289
274
290 if 'name' in nb['metadata']:
275 if 'name' in nb['metadata']:
291 nb['metadata']['name'] = u''
276 nb['metadata']['name'] = u''
292 try:
277 try:
293 self.log.debug("Autosaving notebook %s", os_path)
278 self.log.debug("Autosaving notebook %s", os_path)
294 with io.open(os_path, 'w', encoding='utf-8') as f:
279 with io.open(os_path, 'w', encoding='utf-8') as f:
295 current.write(nb, f, u'json')
280 current.write(nb, f, u'json')
296 except Exception as e:
281 except Exception as e:
297 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
282 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
298
283
299 # Save .py script as well
284 model = self.get(new_name, new_path, content=False)
300 if self.save_script:
301 py_path = os.path.splitext(os_path)[0] + '.py'
302 self.log.debug("Writing script %s", py_path)
303 try:
304 with io.open(py_path, 'w', encoding='utf-8') as f:
305 current.write(nb, f, u'py')
306 except Exception as e:
307 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
308
309 model = self.get_notebook(new_name, new_path, content=False)
310 return model
285 return model
311
286
312 def update_notebook(self, model, name, path=''):
287 def update(self, model, name, path=''):
313 """Update the notebook's path and/or name"""
288 """Update the file's path and/or name"""
314 path = path.strip('/')
289 path = path.strip('/')
315 new_name = model.get('name', name)
290 new_name = model.get('name', name)
316 new_path = model.get('path', path).strip('/')
291 new_path = model.get('path', path).strip('/')
317 if path != new_path or name != new_name:
292 if path != new_path or name != new_name:
318 self.rename_notebook(name, path, new_name, new_path)
293 self.rename(name, path, new_name, new_path)
319 model = self.get_notebook(new_name, new_path, content=False)
294 model = self.get(new_name, new_path, content=False)
320 return model
295 return model
321
296
322 def delete_notebook(self, name, path=''):
297 def delete(self, name, path=''):
323 """Delete notebook by name and path."""
298 """Delete file by name and path."""
324 path = path.strip('/')
299 path = path.strip('/')
325 os_path = self._get_os_path(name, path)
300 os_path = self._get_os_path(name, path)
326 if not os.path.isfile(os_path):
301 if not os.path.isfile(os_path):
327 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
302 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
328
303
329 # clear checkpoints
304 # clear checkpoints
330 for checkpoint in self.list_checkpoints(name, path):
305 for checkpoint in self.list_checkpoints(name, path):
331 checkpoint_id = checkpoint['id']
306 checkpoint_id = checkpoint['id']
332 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
307 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
333 if os.path.isfile(cp_path):
308 if os.path.isfile(cp_path):
334 self.log.debug("Unlinking checkpoint %s", cp_path)
309 self.log.debug("Unlinking checkpoint %s", cp_path)
335 os.unlink(cp_path)
310 os.unlink(cp_path)
336
311
337 self.log.debug("Unlinking notebook %s", os_path)
312 self.log.debug("Unlinking file %s", os_path)
338 os.unlink(os_path)
313 os.unlink(os_path)
339
314
340 def rename_notebook(self, old_name, old_path, new_name, new_path):
315 def rename(self, old_name, old_path, new_name, new_path):
341 """Rename a notebook."""
316 """Rename a file."""
342 old_path = old_path.strip('/')
317 old_path = old_path.strip('/')
343 new_path = new_path.strip('/')
318 new_path = new_path.strip('/')
344 if new_name == old_name and new_path == old_path:
319 if new_name == old_name and new_path == old_path:
345 return
320 return
346
321
347 new_os_path = self._get_os_path(new_name, new_path)
322 new_os_path = self._get_os_path(new_name, new_path)
348 old_os_path = self._get_os_path(old_name, old_path)
323 old_os_path = self._get_os_path(old_name, old_path)
349
324
350 # Should we proceed with the move?
325 # Should we proceed with the move?
351 if os.path.isfile(new_os_path):
326 if os.path.isfile(new_os_path):
352 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
327 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
353 if self.save_script:
354 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
355 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
356 if os.path.isfile(new_py_path):
357 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
358
328
359 # Move the notebook file
329 # Move the file
360 try:
330 try:
361 shutil.move(old_os_path, new_os_path)
331 shutil.move(old_os_path, new_os_path)
362 except Exception as e:
332 except Exception as e:
363 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
333 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
364
334
365 # Move the checkpoints
335 # Move the checkpoints
366 old_checkpoints = self.list_checkpoints(old_name, old_path)
336 old_checkpoints = self.list_checkpoints(old_name, old_path)
367 for cp in old_checkpoints:
337 for cp in old_checkpoints:
368 checkpoint_id = cp['id']
338 checkpoint_id = cp['id']
369 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
339 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
370 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
340 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
371 if os.path.isfile(old_cp_path):
341 if os.path.isfile(old_cp_path):
372 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
342 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
373 shutil.move(old_cp_path, new_cp_path)
343 shutil.move(old_cp_path, new_cp_path)
374
344
375 # Move the .py script
376 if self.save_script:
377 shutil.move(old_py_path, new_py_path)
378
379 # Checkpoint-related utilities
345 # Checkpoint-related utilities
380
346
381 def get_checkpoint_path(self, checkpoint_id, name, path=''):
347 def get_checkpoint_path(self, checkpoint_id, name, path=''):
382 """find the path to a checkpoint"""
348 """find the path to a checkpoint"""
383 path = path.strip('/')
349 path = path.strip('/')
384 basename, _ = os.path.splitext(name)
350 basename, ext = os.path.splitext(name)
385 filename = u"{name}-{checkpoint_id}{ext}".format(
351 filename = u"{name}-{checkpoint_id}{ext}".format(
386 name=basename,
352 name=basename,
387 checkpoint_id=checkpoint_id,
353 checkpoint_id=checkpoint_id,
388 ext=self.filename_ext,
354 ext=ext,
389 )
355 )
390 os_path = self._get_os_path(path=path)
356 os_path = self._get_os_path(path=path)
391 cp_dir = os.path.join(os_path, self.checkpoint_dir)
357 cp_dir = os.path.join(os_path, self.checkpoint_dir)
392 ensure_dir_exists(cp_dir)
358 ensure_dir_exists(cp_dir)
393 cp_path = os.path.join(cp_dir, filename)
359 cp_path = os.path.join(cp_dir, filename)
394 return cp_path
360 return cp_path
395
361
396 def get_checkpoint_model(self, checkpoint_id, name, path=''):
362 def get_checkpoint_model(self, checkpoint_id, name, path=''):
397 """construct the info dict for a given checkpoint"""
363 """construct the info dict for a given checkpoint"""
398 path = path.strip('/')
364 path = path.strip('/')
399 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
365 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
400 stats = os.stat(cp_path)
366 stats = os.stat(cp_path)
401 last_modified = tz.utcfromtimestamp(stats.st_mtime)
367 last_modified = tz.utcfromtimestamp(stats.st_mtime)
402 info = dict(
368 info = dict(
403 id = checkpoint_id,
369 id = checkpoint_id,
404 last_modified = last_modified,
370 last_modified = last_modified,
405 )
371 )
406 return info
372 return info
407
373
408 # public checkpoint API
374 # public checkpoint API
409
375
410 def create_checkpoint(self, name, path=''):
376 def create_checkpoint(self, name, path=''):
411 """Create a checkpoint from the current state of a notebook"""
377 """Create a checkpoint from the current state of a file"""
412 path = path.strip('/')
378 path = path.strip('/')
413 nb_path = self._get_os_path(name, path)
379 src_path = self._get_os_path(name, path)
414 # only the one checkpoint ID:
380 # only the one checkpoint ID:
415 checkpoint_id = u"checkpoint"
381 checkpoint_id = u"checkpoint"
416 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
382 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
417 self.log.debug("creating checkpoint for notebook %s", name)
383 self.log.debug("creating checkpoint for notebook %s", name)
418 self._copy(nb_path, cp_path)
384 self._copy(src_path, cp_path)
419
385
420 # return the checkpoint info
386 # return the checkpoint info
421 return self.get_checkpoint_model(checkpoint_id, name, path)
387 return self.get_checkpoint_model(checkpoint_id, name, path)
422
388
423 def list_checkpoints(self, name, path=''):
389 def list_checkpoints(self, name, path=''):
424 """list the checkpoints for a given notebook
390 """list the checkpoints for a given file
425
391
426 This notebook manager currently only supports one checkpoint per notebook.
392 This contents manager currently only supports one checkpoint per file.
427 """
393 """
428 path = path.strip('/')
394 path = path.strip('/')
429 checkpoint_id = "checkpoint"
395 checkpoint_id = "checkpoint"
430 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
396 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
431 if not os.path.exists(os_path):
397 if not os.path.exists(os_path):
432 return []
398 return []
433 else:
399 else:
434 return [self.get_checkpoint_model(checkpoint_id, name, path)]
400 return [self.get_checkpoint_model(checkpoint_id, name, path)]
435
401
436
402
437 def restore_checkpoint(self, checkpoint_id, name, path=''):
403 def restore_checkpoint(self, checkpoint_id, name, path=''):
438 """restore a notebook to a checkpointed state"""
404 """restore a file to a checkpointed state"""
439 path = path.strip('/')
405 path = path.strip('/')
440 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
406 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
441 nb_path = self._get_os_path(name, path)
407 nb_path = self._get_os_path(name, path)
442 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
408 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
443 if not os.path.isfile(cp_path):
409 if not os.path.isfile(cp_path):
444 self.log.debug("checkpoint file does not exist: %s", cp_path)
410 self.log.debug("checkpoint file does not exist: %s", cp_path)
445 raise web.HTTPError(404,
411 raise web.HTTPError(404,
446 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
412 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
447 )
413 )
448 # ensure notebook is readable (never restore from an unreadable notebook)
414 # ensure notebook is readable (never restore from an unreadable notebook)
449 with io.open(cp_path, 'r', encoding='utf-8') as f:
415 if cp_path.endswith('.ipynb'):
450 current.read(f, u'json')
416 with io.open(cp_path, 'r', encoding='utf-8') as f:
417 current.read(f, u'json')
451 self._copy(cp_path, nb_path)
418 self._copy(cp_path, nb_path)
452 self.log.debug("copying %s -> %s", cp_path, nb_path)
419 self.log.debug("copying %s -> %s", cp_path, nb_path)
453
420
454 def delete_checkpoint(self, checkpoint_id, name, path=''):
421 def delete_checkpoint(self, checkpoint_id, name, path=''):
455 """delete a notebook's checkpoint"""
422 """delete a file's checkpoint"""
456 path = path.strip('/')
423 path = path.strip('/')
457 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
424 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
458 if not os.path.isfile(cp_path):
425 if not os.path.isfile(cp_path):
459 raise web.HTTPError(404,
426 raise web.HTTPError(404,
460 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
427 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
461 )
428 )
462 self.log.debug("unlinking %s", cp_path)
429 self.log.debug("unlinking %s", cp_path)
463 os.unlink(cp_path)
430 os.unlink(cp_path)
464
431
465 def info_string(self):
432 def info_string(self):
466 return "Serving notebooks from local directory: %s" % self.notebook_dir
433 return "Serving notebooks from local directory: %s" % self.root_dir
467
434
468 def get_kernel_path(self, name, path='', model=None):
435 def get_kernel_path(self, name, path='', model=None):
469 """ Return the path to start kernel in """
436 """Return the initial working dir a kernel associated with a given notebook"""
470 return os.path.join(self.notebook_dir, path)
437 return os.path.join(self.root_dir, path)
@@ -1,287 +1,270 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the contents web service."""
2
2
3 Authors:
3 # Copyright (c) IPython Development Team.
4
4 # Distributed under the terms of the Modified BSD License.
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
5
19 import json
6 import json
20
7
21 from tornado import web
8 from tornado import web
22
9
23 from IPython.html.utils import url_path_join, url_escape
10 from IPython.html.utils import url_path_join, url_escape
24 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
25
12
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 notebook_path_regex, path_regex,
14 notebook_path_regex, path_regex,
28 notebook_name_regex)
15 notebook_name_regex)
29
16
30 #-----------------------------------------------------------------------------
31 # Notebook web service handlers
32 #-----------------------------------------------------------------------------
33
34
17
35 class NotebookHandler(IPythonHandler):
18 class ContentsHandler(IPythonHandler):
36
19
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
20 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
38
21
39 def notebook_location(self, name, path=''):
22 def location_url(self, name, path=''):
40 """Return the full URL location of a notebook based.
23 """Return the full URL location of a file.
41
24
42 Parameters
25 Parameters
43 ----------
26 ----------
44 name : unicode
27 name : unicode
45 The base name of the notebook, such as "foo.ipynb".
28 The base name of the file, such as "foo.ipynb".
46 path : unicode
29 path : unicode
47 The URL path of the notebook.
30 The API path of the file, such as "foo/bar".
48 """
31 """
49 return url_escape(url_path_join(
32 return url_escape(url_path_join(
50 self.base_url, 'api', 'notebooks', path, name
33 self.base_url, 'api', 'contents', path, name
51 ))
34 ))
52
35
53 def _finish_model(self, model, location=True):
36 def _finish_model(self, model, location=True):
54 """Finish a JSON request with a model, setting relevant headers, etc."""
37 """Finish a JSON request with a model, setting relevant headers, etc."""
55 if location:
38 if location:
56 location = self.notebook_location(model['name'], model['path'])
39 location = self.location_url(model['name'], model['path'])
57 self.set_header('Location', location)
40 self.set_header('Location', location)
58 self.set_header('Last-Modified', model['last_modified'])
41 self.set_header('Last-Modified', model['last_modified'])
59 self.finish(json.dumps(model, default=date_default))
42 self.finish(json.dumps(model, default=date_default))
60
43
61 @web.authenticated
44 @web.authenticated
62 @json_errors
45 @json_errors
63 def get(self, path='', name=None):
46 def get(self, path='', name=None):
64 """Return a Notebook or list of notebooks.
47 """Return a file or list of files.
65
48
66 * GET with path and no notebook name lists notebooks in a directory
49 * GET with path and no filename lists files in a directory
67 * GET with path and notebook name returns notebook JSON
50 * GET with path and filename returns file contents model
68 """
51 """
69 nbm = self.notebook_manager
52 cm = self.contents_manager
70 # Check to see if a notebook name was given
53 # Check to see if a filename was given
71 if name is None:
54 if name is None:
72 # TODO: Remove this after we create the contents web service and directories are
55 # TODO: Remove this after we create the contents web service and directories are
73 # no longer listed by the notebook web service. This should only handle notebooks
56 # no longer listed by the notebook web service. This should only handle notebooks
74 # and not directories.
57 # and not directories.
75 dirs = nbm.list_dirs(path)
58 dirs = cm.list_dirs(path)
76 notebooks = []
59 files = []
77 index = []
60 index = []
78 for nb in nbm.list_notebooks(path):
61 for nb in cm.list_files(path):
79 if nb['name'].lower() == 'index.ipynb':
62 if nb['name'].lower() == 'index.ipynb':
80 index.append(nb)
63 index.append(nb)
81 else:
64 else:
82 notebooks.append(nb)
65 files.append(nb)
83 notebooks = index + dirs + notebooks
66 files = index + dirs + files
84 self.finish(json.dumps(notebooks, default=date_default))
67 self.finish(json.dumps(files, default=date_default))
85 return
68 return
86 # get and return notebook representation
69 # get and return notebook representation
87 model = nbm.get_notebook(name, path)
70 model = cm.get(name, path)
88 self._finish_model(model, location=False)
71 self._finish_model(model, location=False)
89
72
90 @web.authenticated
73 @web.authenticated
91 @json_errors
74 @json_errors
92 def patch(self, path='', name=None):
75 def patch(self, path='', name=None):
93 """PATCH renames a notebook without re-uploading content."""
76 """PATCH renames a notebook without re-uploading content."""
94 nbm = self.notebook_manager
77 cm = self.contents_manager
95 if name is None:
78 if name is None:
96 raise web.HTTPError(400, u'Notebook name missing')
79 raise web.HTTPError(400, u'Filename missing')
97 model = self.get_json_body()
80 model = self.get_json_body()
98 if model is None:
81 if model is None:
99 raise web.HTTPError(400, u'JSON body missing')
82 raise web.HTTPError(400, u'JSON body missing')
100 model = nbm.update_notebook(model, name, path)
83 model = cm.update(model, name, path)
101 self._finish_model(model)
84 self._finish_model(model)
102
85
103 def _copy_notebook(self, copy_from, path, copy_to=None):
86 def _copy(self, copy_from, path, copy_to=None):
104 """Copy a notebook in path, optionally specifying the new name.
87 """Copy a file in path, optionally specifying the new name.
105
88
106 Only support copying within the same directory.
89 Only support copying within the same directory.
107 """
90 """
108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
91 self.log.info(u"Copying from %s/%s to %s/%s",
109 path, copy_from,
92 path, copy_from,
110 path, copy_to or '',
93 path, copy_to or '',
111 )
94 )
112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
95 model = self.contents_manager.copy(copy_from, copy_to, path)
113 self.set_status(201)
96 self.set_status(201)
114 self._finish_model(model)
97 self._finish_model(model)
115
98
116 def _upload_notebook(self, model, path, name=None):
99 def _upload(self, model, path, name=None):
117 """Upload a notebook
100 """Upload a file
118
101
119 If name specified, create it in path/name.
102 If name specified, create it in path/name.
120 """
103 """
121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
104 self.log.info(u"Uploading file to %s/%s", path, name or '')
122 if name:
105 if name:
123 model['name'] = name
106 model['name'] = name
124
107
125 model = self.notebook_manager.create_notebook(model, path)
108 model = self.contents_manager.create_notebook(model, path)
126 self.set_status(201)
109 self.set_status(201)
127 self._finish_model(model)
110 self._finish_model(model)
128
111
129 def _create_empty_notebook(self, path, name=None):
112 def _create_empty_notebook(self, path, name=None):
130 """Create an empty notebook in path
113 """Create an empty notebook in path
131
114
132 If name specified, create it in path/name.
115 If name specified, create it in path/name.
133 """
116 """
134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
117 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
135 model = {}
118 model = {}
136 if name:
119 if name:
137 model['name'] = name
120 model['name'] = name
138 model = self.notebook_manager.create_notebook(model, path=path)
121 model = self.contents_manager.create_notebook(model, path=path)
139 self.set_status(201)
122 self.set_status(201)
140 self._finish_model(model)
123 self._finish_model(model)
141
124
142 def _save_notebook(self, model, path, name):
125 def _save(self, model, path, name):
143 """Save an existing notebook."""
126 """Save an existing file."""
144 self.log.info(u"Saving notebook at %s/%s", path, name)
127 self.log.info(u"Saving file at %s/%s", path, name)
145 model = self.notebook_manager.save_notebook(model, name, path)
128 model = self.contents_manager.save(model, name, path)
146 if model['path'] != path.strip('/') or model['name'] != name:
129 if model['path'] != path.strip('/') or model['name'] != name:
147 # a rename happened, set Location header
130 # a rename happened, set Location header
148 location = True
131 location = True
149 else:
132 else:
150 location = False
133 location = False
151 self._finish_model(model, location)
134 self._finish_model(model, location)
152
135
153 @web.authenticated
136 @web.authenticated
154 @json_errors
137 @json_errors
155 def post(self, path='', name=None):
138 def post(self, path='', name=None):
156 """Create a new notebook in the specified path.
139 """Create a new notebook in the specified path.
157
140
158 POST creates new notebooks. The server always decides on the notebook name.
141 POST creates new notebooks. The server always decides on the notebook name.
159
142
160 POST /api/notebooks/path
143 POST /api/contents/path
161 New untitled notebook in path. If content specified, upload a
144 New untitled notebook in path. If content specified, upload a
162 notebook, otherwise start empty.
145 notebook, otherwise start empty.
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
146 POST /api/contents/path?copy=OtherNotebook.ipynb
164 New copy of OtherNotebook in path
147 New copy of OtherNotebook in path
165 """
148 """
166
149
167 if name is not None:
150 if name is not None:
168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
151 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
169
152
170 model = self.get_json_body()
153 model = self.get_json_body()
171
154
172 if model is not None:
155 if model is not None:
173 copy_from = model.get('copy_from')
156 copy_from = model.get('copy_from')
174 if copy_from:
157 if copy_from:
175 if model.get('content'):
158 if model.get('content'):
176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
159 raise web.HTTPError(400, "Can't upload and copy at the same time.")
177 self._copy_notebook(copy_from, path)
160 self._copy(copy_from, path)
178 else:
161 else:
179 self._upload_notebook(model, path)
162 self._upload(model, path)
180 else:
163 else:
181 self._create_empty_notebook(path)
164 self._create_empty_notebook(path)
182
165
183 @web.authenticated
166 @web.authenticated
184 @json_errors
167 @json_errors
185 def put(self, path='', name=None):
168 def put(self, path='', name=None):
186 """Saves the notebook in the location specified by name and path.
169 """Saves the file in the location specified by name and path.
187
170
188 PUT is very similar to POST, but the requester specifies the name,
171 PUT is very similar to POST, but the requester specifies the name,
189 whereas with POST, the server picks the name.
172 whereas with POST, the server picks the name.
190
173
191 PUT /api/notebooks/path/Name.ipynb
174 PUT /api/contents/path/Name.ipynb
192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
175 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
193 in `content` key of JSON request body. If content is not specified,
176 in `content` key of JSON request body. If content is not specified,
194 create a new empty notebook.
177 create a new empty notebook.
195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
178 PUT /api/contents/path/Name.ipynb?copy=OtherNotebook.ipynb
196 Copy OtherNotebook to Name
179 Copy OtherNotebook to Name
197 """
180 """
198 if name is None:
181 if name is None:
199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
182 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
200
183
201 model = self.get_json_body()
184 model = self.get_json_body()
202 if model:
185 if model:
203 copy_from = model.get('copy_from')
186 copy_from = model.get('copy_from')
204 if copy_from:
187 if copy_from:
205 if model.get('content'):
188 if model.get('content'):
206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
189 raise web.HTTPError(400, "Can't upload and copy at the same time.")
207 self._copy_notebook(copy_from, path, name)
190 self._copy(copy_from, path, name)
208 elif self.notebook_manager.notebook_exists(name, path):
191 elif self.contents_manager.file_exists(name, path):
209 self._save_notebook(model, path, name)
192 self._save(model, path, name)
210 else:
193 else:
211 self._upload_notebook(model, path, name)
194 self._upload(model, path, name)
212 else:
195 else:
213 self._create_empty_notebook(path, name)
196 self._create_empty_notebook(path, name)
214
197
215 @web.authenticated
198 @web.authenticated
216 @json_errors
199 @json_errors
217 def delete(self, path='', name=None):
200 def delete(self, path='', name=None):
218 """delete the notebook in the given notebook path"""
201 """delete a file in the given path"""
219 nbm = self.notebook_manager
202 cm = self.contents_manager
220 nbm.delete_notebook(name, path)
203 cm.delete(name, path)
221 self.set_status(204)
204 self.set_status(204)
222 self.finish()
205 self.finish()
223
206
224
207
225 class NotebookCheckpointsHandler(IPythonHandler):
208 class CheckpointsHandler(IPythonHandler):
226
209
227 SUPPORTED_METHODS = ('GET', 'POST')
210 SUPPORTED_METHODS = ('GET', 'POST')
228
211
229 @web.authenticated
212 @web.authenticated
230 @json_errors
213 @json_errors
231 def get(self, path='', name=None):
214 def get(self, path='', name=None):
232 """get lists checkpoints for a notebook"""
215 """get lists checkpoints for a file"""
233 nbm = self.notebook_manager
216 cm = self.contents_manager
234 checkpoints = nbm.list_checkpoints(name, path)
217 checkpoints = cm.list_checkpoints(name, path)
235 data = json.dumps(checkpoints, default=date_default)
218 data = json.dumps(checkpoints, default=date_default)
236 self.finish(data)
219 self.finish(data)
237
220
238 @web.authenticated
221 @web.authenticated
239 @json_errors
222 @json_errors
240 def post(self, path='', name=None):
223 def post(self, path='', name=None):
241 """post creates a new checkpoint"""
224 """post creates a new checkpoint"""
242 nbm = self.notebook_manager
225 cm = self.contents_manager
243 checkpoint = nbm.create_checkpoint(name, path)
226 checkpoint = cm.create_checkpoint(name, path)
244 data = json.dumps(checkpoint, default=date_default)
227 data = json.dumps(checkpoint, default=date_default)
245 location = url_path_join(self.base_url, 'api/notebooks',
228 location = url_path_join(self.base_url, 'api/contents',
246 path, name, 'checkpoints', checkpoint['id'])
229 path, name, 'checkpoints', checkpoint['id'])
247 self.set_header('Location', url_escape(location))
230 self.set_header('Location', url_escape(location))
248 self.set_status(201)
231 self.set_status(201)
249 self.finish(data)
232 self.finish(data)
250
233
251
234
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
235 class ModifyCheckpointsHandler(IPythonHandler):
253
236
254 SUPPORTED_METHODS = ('POST', 'DELETE')
237 SUPPORTED_METHODS = ('POST', 'DELETE')
255
238
256 @web.authenticated
239 @web.authenticated
257 @json_errors
240 @json_errors
258 def post(self, path, name, checkpoint_id):
241 def post(self, path, name, checkpoint_id):
259 """post restores a notebook from a checkpoint"""
242 """post restores a file from a checkpoint"""
260 nbm = self.notebook_manager
243 cm = self.contents_manager
261 nbm.restore_checkpoint(checkpoint_id, name, path)
244 cm.restore_checkpoint(checkpoint_id, name, path)
262 self.set_status(204)
245 self.set_status(204)
263 self.finish()
246 self.finish()
264
247
265 @web.authenticated
248 @web.authenticated
266 @json_errors
249 @json_errors
267 def delete(self, path, name, checkpoint_id):
250 def delete(self, path, name, checkpoint_id):
268 """delete clears a checkpoint for a given notebook"""
251 """delete clears a checkpoint for a given file"""
269 nbm = self.notebook_manager
252 cm = self.contents_manager
270 nbm.delete_checkpoint(checkpoint_id, name, path)
253 cm.delete_checkpoint(checkpoint_id, name, path)
271 self.set_status(204)
254 self.set_status(204)
272 self.finish()
255 self.finish()
273
256
274 #-----------------------------------------------------------------------------
257 #-----------------------------------------------------------------------------
275 # URL to handler mappings
258 # URL to handler mappings
276 #-----------------------------------------------------------------------------
259 #-----------------------------------------------------------------------------
277
260
278
261
279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
262 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
280
263
281 default_handlers = [
264 default_handlers = [
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
265 (r"/api/contents%s/checkpoints" % notebook_path_regex, CheckpointsHandler),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
266 (r"/api/contents%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
284 ModifyNotebookCheckpointsHandler),
267 ModifyCheckpointsHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
268 (r"/api/contents%s" % notebook_path_regex, ContentsHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
269 (r"/api/contents%s" % path_regex, ContentsHandler),
287 ]
270 ]
@@ -1,287 +1,267 b''
1 """A base class notebook manager.
1 """A base class for contents managers."""
2
2
3 Authors:
3 # Copyright (c) IPython Development Team.
4
4 # Distributed under the terms of the Modified BSD License.
5 * Brian Granger
6 * Zach Sailer
7 """
8
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
11 #
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
15
16 #-----------------------------------------------------------------------------
17 # Imports
18 #-----------------------------------------------------------------------------
19
5
20 from fnmatch import fnmatch
6 from fnmatch import fnmatch
21 import itertools
7 import itertools
22 import os
8 import os
23
9
24 from IPython.config.configurable import LoggingConfigurable
10 from IPython.config.configurable import LoggingConfigurable
25 from IPython.nbformat import current, sign
11 from IPython.nbformat import current, sign
26 from IPython.utils.traitlets import Instance, Unicode, List
12 from IPython.utils.traitlets import Instance, Unicode, List
27
13
28 #-----------------------------------------------------------------------------
29 # Classes
30 #-----------------------------------------------------------------------------
31
14
32 class NotebookManager(LoggingConfigurable):
15 class ContentsManager(LoggingConfigurable):
33
34 filename_ext = Unicode(u'.ipynb')
35
16
36 notary = Instance(sign.NotebookNotary)
17 notary = Instance(sign.NotebookNotary)
37 def _notary_default(self):
18 def _notary_default(self):
38 return sign.NotebookNotary(parent=self)
19 return sign.NotebookNotary(parent=self)
39
20
40 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
21 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
41 Glob patterns to hide in file and directory listings.
22 Glob patterns to hide in file and directory listings.
42 """)
23 """)
43
24
44 # NotebookManager API part 1: methods that must be
25 # ContentsManager API part 1: methods that must be
45 # implemented in subclasses.
26 # implemented in subclasses.
46
27
47 def path_exists(self, path):
28 def path_exists(self, path):
48 """Does the API-style path (directory) actually exist?
29 """Does the API-style path (directory) actually exist?
49
30
50 Override this method in subclasses.
31 Override this method in subclasses.
51
32
52 Parameters
33 Parameters
53 ----------
34 ----------
54 path : string
35 path : string
55 The path to check
36 The path to check
56
37
57 Returns
38 Returns
58 -------
39 -------
59 exists : bool
40 exists : bool
60 Whether the path does indeed exist.
41 Whether the path does indeed exist.
61 """
42 """
62 raise NotImplementedError
43 raise NotImplementedError
63
44
64 def is_hidden(self, path):
45 def is_hidden(self, path):
65 """Does the API style path correspond to a hidden directory or file?
46 """Does the API style path correspond to a hidden directory or file?
66
47
67 Parameters
48 Parameters
68 ----------
49 ----------
69 path : string
50 path : string
70 The path to check. This is an API path (`/` separated,
51 The path to check. This is an API path (`/` separated,
71 relative to base notebook-dir).
52 relative to root dir).
72
53
73 Returns
54 Returns
74 -------
55 -------
75 exists : bool
56 exists : bool
76 Whether the path is hidden.
57 Whether the path is hidden.
77
58
78 """
59 """
79 raise NotImplementedError
60 raise NotImplementedError
80
61
81 def notebook_exists(self, name, path=''):
62 def file_exists(self, name, path=''):
82 """Returns a True if the notebook exists. Else, returns False.
63 """Returns a True if the notebook exists. Else, returns False.
83
64
84 Parameters
65 Parameters
85 ----------
66 ----------
86 name : string
67 name : string
87 The name of the notebook you are checking.
68 The name of the notebook you are checking.
88 path : string
69 path : string
89 The relative path to the notebook (with '/' as separator)
70 The relative path to the notebook (with '/' as separator)
90
71
91 Returns
72 Returns
92 -------
73 -------
93 bool
74 bool
94 """
75 """
95 raise NotImplementedError('must be implemented in a subclass')
76 raise NotImplementedError('must be implemented in a subclass')
96
77
97 # TODO: Remove this after we create the contents web service and directories are
78 # TODO: Remove this after we create the contents web service and directories are
98 # no longer listed by the notebook web service.
79 # no longer listed by the notebook web service.
99 def list_dirs(self, path):
80 def list_dirs(self, path):
100 """List the directory models for a given API style path."""
81 """List the directory models for a given API style path."""
101 raise NotImplementedError('must be implemented in a subclass')
82 raise NotImplementedError('must be implemented in a subclass')
102
83
103 # TODO: Remove this after we create the contents web service and directories are
84 # TODO: Remove this after we create the contents web service and directories are
104 # no longer listed by the notebook web service.
85 # no longer listed by the notebook web service.
105 def get_dir_model(self, name, path=''):
86 def get_dir_model(self, name, path=''):
106 """Get the directory model given a directory name and its API style path.
87 """Get the directory model given a directory name and its API style path.
107
88
108 The keys in the model should be:
89 The keys in the model should be:
109 * name
90 * name
110 * path
91 * path
111 * last_modified
92 * last_modified
112 * created
93 * created
113 * type='directory'
94 * type='directory'
114 """
95 """
115 raise NotImplementedError('must be implemented in a subclass')
96 raise NotImplementedError('must be implemented in a subclass')
116
97
117 def list_notebooks(self, path=''):
98 def list_files(self, path=''):
118 """Return a list of notebook dicts without content.
99 """Return a list of contents dicts without content.
119
120 This returns a list of dicts, each of the form::
121
100
122 dict(notebook_id=notebook,name=name)
101 This returns a list of dicts
123
102
124 This list of dicts should be sorted by name::
103 This list of dicts should be sorted by name::
125
104
126 data = sorted(data, key=lambda item: item['name'])
105 data = sorted(data, key=lambda item: item['name'])
127 """
106 """
128 raise NotImplementedError('must be implemented in a subclass')
107 raise NotImplementedError('must be implemented in a subclass')
129
108
130 def get_notebook(self, name, path='', content=True):
109 def get_model(self, name, path='', content=True):
131 """Get the notebook model with or without content."""
110 """Get the notebook model with or without content."""
132 raise NotImplementedError('must be implemented in a subclass')
111 raise NotImplementedError('must be implemented in a subclass')
133
112
134 def save_notebook(self, model, name, path=''):
113 def save(self, model, name, path=''):
135 """Save the notebook and return the model with no content."""
114 """Save the notebook and return the model with no content."""
136 raise NotImplementedError('must be implemented in a subclass')
115 raise NotImplementedError('must be implemented in a subclass')
137
116
138 def update_notebook(self, model, name, path=''):
117 def update(self, model, name, path=''):
139 """Update the notebook and return the model with no content."""
118 """Update the notebook and return the model with no content."""
140 raise NotImplementedError('must be implemented in a subclass')
119 raise NotImplementedError('must be implemented in a subclass')
141
120
142 def delete_notebook(self, name, path=''):
121 def delete(self, name, path=''):
143 """Delete notebook by name and path."""
122 """Delete notebook by name and path."""
144 raise NotImplementedError('must be implemented in a subclass')
123 raise NotImplementedError('must be implemented in a subclass')
145
124
146 def create_checkpoint(self, name, path=''):
125 def create_checkpoint(self, name, path=''):
147 """Create a checkpoint of the current state of a notebook
126 """Create a checkpoint of the current state of a notebook
148
127
149 Returns a checkpoint_id for the new checkpoint.
128 Returns a checkpoint_id for the new checkpoint.
150 """
129 """
151 raise NotImplementedError("must be implemented in a subclass")
130 raise NotImplementedError("must be implemented in a subclass")
152
131
153 def list_checkpoints(self, name, path=''):
132 def list_checkpoints(self, name, path=''):
154 """Return a list of checkpoints for a given notebook"""
133 """Return a list of checkpoints for a given notebook"""
155 return []
134 return []
156
135
157 def restore_checkpoint(self, checkpoint_id, name, path=''):
136 def restore_checkpoint(self, checkpoint_id, name, path=''):
158 """Restore a notebook from one of its checkpoints"""
137 """Restore a notebook from one of its checkpoints"""
159 raise NotImplementedError("must be implemented in a subclass")
138 raise NotImplementedError("must be implemented in a subclass")
160
139
161 def delete_checkpoint(self, checkpoint_id, name, path=''):
140 def delete_checkpoint(self, checkpoint_id, name, path=''):
162 """delete a checkpoint for a notebook"""
141 """delete a checkpoint for a notebook"""
163 raise NotImplementedError("must be implemented in a subclass")
142 raise NotImplementedError("must be implemented in a subclass")
164
143
165 def info_string(self):
144 def info_string(self):
166 return "Serving notebooks"
145 return "Serving notebooks"
167
146
168 # NotebookManager API part 2: methods that have useable default
147 # ContentsManager API part 2: methods that have useable default
169 # implementations, but can be overridden in subclasses.
148 # implementations, but can be overridden in subclasses.
170
149
171 def get_kernel_path(self, name, path='', model=None):
150 def get_kernel_path(self, name, path='', model=None):
172 """ Return the path to start kernel in """
151 """ Return the path to start kernel in """
173 return path
152 return path
174
153
175 def increment_filename(self, basename, path=''):
154 def increment_filename(self, filename, path=''):
176 """Increment a notebook filename without the .ipynb to make it unique.
155 """Increment a filename until it is unique.
177
156
178 Parameters
157 Parameters
179 ----------
158 ----------
180 basename : unicode
159 filename : unicode
181 The name of a notebook without the ``.ipynb`` file extension.
160 The name of a file, including extension
182 path : unicode
161 path : unicode
183 The URL path of the notebooks directory
162 The URL path of the notebooks directory
184
163
185 Returns
164 Returns
186 -------
165 -------
187 name : unicode
166 name : unicode
188 A notebook name (with the .ipynb extension) that starts
167 A filename that is unique, based on the input filename.
189 with basename and does not refer to any existing notebook.
190 """
168 """
191 path = path.strip('/')
169 path = path.strip('/')
170 basename, ext = os.path.splitext(filename)
192 for i in itertools.count():
171 for i in itertools.count():
193 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
172 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
194 ext=self.filename_ext)
173 ext=ext)
195 if not self.notebook_exists(name, path):
174 if not self.file_exists(name, path):
196 break
175 break
197 return name
176 return name
198
177
199 def create_notebook(self, model=None, path=''):
178 def create_notebook(self, model=None, path=''):
200 """Create a new notebook and return its model with no content."""
179 """Create a new notebook and return its model with no content."""
201 path = path.strip('/')
180 path = path.strip('/')
202 if model is None:
181 if model is None:
203 model = {}
182 model = {}
204 if 'content' not in model:
183 if 'content' not in model:
205 metadata = current.new_metadata(name=u'')
184 metadata = current.new_metadata(name=u'')
206 model['content'] = current.new_notebook(metadata=metadata)
185 model['content'] = current.new_notebook(metadata=metadata)
207 if 'name' not in model:
186 if 'name' not in model:
208 model['name'] = self.increment_filename('Untitled', path)
187 model['name'] = self.increment_filename('Untitled.ipynb', path)
209
188
210 model['path'] = path
189 model['path'] = path
211 model = self.save_notebook(model, model['name'], model['path'])
190 model = self.save(model, model['name'], model['path'])
212 return model
191 return model
213
192
214 def copy_notebook(self, from_name, to_name=None, path=''):
193 def copy(self, from_name, to_name=None, path=''):
215 """Copy an existing notebook and return its new model.
194 """Copy an existing file and return its new model.
216
195
217 If to_name not specified, increment `from_name-Copy#.ipynb`.
196 If to_name not specified, increment `from_name-Copy#.ipynb`.
218 """
197 """
219 path = path.strip('/')
198 path = path.strip('/')
220 model = self.get_notebook(from_name, path)
199 model = self.get(from_name, path)
221 if not to_name:
200 if not to_name:
222 base = os.path.splitext(from_name)[0] + '-Copy'
201 base, ext = os.path.splitext(from_name)
223 to_name = self.increment_filename(base, path)
202 copy_name = u'{0}-Copy{1}'.format(base, ext)
203 to_name = self.increment_filename(copy_name, path)
224 model['name'] = to_name
204 model['name'] = to_name
225 model = self.save_notebook(model, to_name, path)
205 model = self.save(model, to_name, path)
226 return model
206 return model
227
207
228 def log_info(self):
208 def log_info(self):
229 self.log.info(self.info_string())
209 self.log.info(self.info_string())
230
210
231 def trust_notebook(self, name, path=''):
211 def trust_notebook(self, name, path=''):
232 """Explicitly trust a notebook
212 """Explicitly trust a notebook
233
213
234 Parameters
214 Parameters
235 ----------
215 ----------
236 name : string
216 name : string
237 The filename of the notebook
217 The filename of the notebook
238 path : string
218 path : string
239 The notebook's directory
219 The notebook's directory
240 """
220 """
241 model = self.get_notebook(name, path)
221 model = self.get(name, path)
242 nb = model['content']
222 nb = model['content']
243 self.log.warn("Trusting notebook %s/%s", path, name)
223 self.log.warn("Trusting notebook %s/%s", path, name)
244 self.notary.mark_cells(nb, True)
224 self.notary.mark_cells(nb, True)
245 self.save_notebook(model, name, path)
225 self.save(model, name, path)
246
226
247 def check_and_sign(self, nb, name, path=''):
227 def check_and_sign(self, nb, name, path=''):
248 """Check for trusted cells, and sign the notebook.
228 """Check for trusted cells, and sign the notebook.
249
229
250 Called as a part of saving notebooks.
230 Called as a part of saving notebooks.
251
231
252 Parameters
232 Parameters
253 ----------
233 ----------
254 nb : dict
234 nb : dict
255 The notebook structure
235 The notebook structure
256 name : string
236 name : string
257 The filename of the notebook
237 The filename of the notebook
258 path : string
238 path : string
259 The notebook's directory
239 The notebook's directory
260 """
240 """
261 if self.notary.check_cells(nb):
241 if self.notary.check_cells(nb):
262 self.notary.sign(nb)
242 self.notary.sign(nb)
263 else:
243 else:
264 self.log.warn("Saving untrusted notebook %s/%s", path, name)
244 self.log.warn("Saving untrusted notebook %s/%s", path, name)
265
245
266 def mark_trusted_cells(self, nb, name, path=''):
246 def mark_trusted_cells(self, nb, name, path=''):
267 """Mark cells as trusted if the notebook signature matches.
247 """Mark cells as trusted if the notebook signature matches.
268
248
269 Called as a part of loading notebooks.
249 Called as a part of loading notebooks.
270
250
271 Parameters
251 Parameters
272 ----------
252 ----------
273 nb : dict
253 nb : dict
274 The notebook structure
254 The notebook structure
275 name : string
255 name : string
276 The filename of the notebook
256 The filename of the notebook
277 path : string
257 path : string
278 The notebook's directory
258 The notebook's directory
279 """
259 """
280 trusted = self.notary.check_signature(nb)
260 trusted = self.notary.check_signature(nb)
281 if not trusted:
261 if not trusted:
282 self.log.warn("Notebook %s/%s is not trusted", path, name)
262 self.log.warn("Notebook %s/%s is not trusted", path, name)
283 self.notary.mark_cells(nb, trusted)
263 self.notary.mark_cells(nb, trusted)
284
264
285 def should_list(self, name):
265 def should_list(self, name):
286 """Should this file/directory name be displayed in a listing?"""
266 """Should this file/directory name be displayed in a listing?"""
287 return not any(fnmatch(name, glob) for glob in self.hide_globs)
267 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,346 +1,346 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the notebooks webservice API."""
2 """Test the contents webservice API."""
3
3
4 import io
4 import io
5 import json
5 import json
6 import os
6 import os
7 import shutil
7 import shutil
8 from unicodedata import normalize
8 from unicodedata import normalize
9
9
10 pjoin = os.path.join
10 pjoin = os.path.join
11
11
12 import requests
12 import requests
13
13
14 from IPython.html.utils import url_path_join, url_escape
14 from IPython.html.utils import url_path_join, url_escape
15 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
15 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 from IPython.nbformat import current
16 from IPython.nbformat import current
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 new_heading_cell, to_notebook_json)
18 new_heading_cell, to_notebook_json)
19 from IPython.nbformat import v2
19 from IPython.nbformat import v2
20 from IPython.utils import py3compat
20 from IPython.utils import py3compat
21 from IPython.utils.data import uniq_stable
21 from IPython.utils.data import uniq_stable
22
22
23
23
24 # TODO: Remove this after we create the contents web service and directories are
24 # TODO: Remove this after we create the contents web service and directories are
25 # no longer listed by the notebook web service.
25 # no longer listed by the notebook web service.
26 def notebooks_only(nb_list):
26 def notebooks_only(nb_list):
27 return [nb for nb in nb_list if nb['type']=='notebook']
27 return [nb for nb in nb_list if nb['type']=='notebook']
28
28
29 def dirs_only(nb_list):
29 def dirs_only(nb_list):
30 return [x for x in nb_list if x['type']=='directory']
30 return [x for x in nb_list if x['type']=='directory']
31
31
32
32
33 class NBAPI(object):
33 class API(object):
34 """Wrapper for notebook API calls."""
34 """Wrapper for contents API calls."""
35 def __init__(self, base_url):
35 def __init__(self, base_url):
36 self.base_url = base_url
36 self.base_url = base_url
37
37
38 def _req(self, verb, path, body=None):
38 def _req(self, verb, path, body=None):
39 response = requests.request(verb,
39 response = requests.request(verb,
40 url_path_join(self.base_url, 'api/notebooks', path),
40 url_path_join(self.base_url, 'api/contents', path),
41 data=body,
41 data=body,
42 )
42 )
43 response.raise_for_status()
43 response.raise_for_status()
44 return response
44 return response
45
45
46 def list(self, path='/'):
46 def list(self, path='/'):
47 return self._req('GET', path)
47 return self._req('GET', path)
48
48
49 def read(self, name, path='/'):
49 def read(self, name, path='/'):
50 return self._req('GET', url_path_join(path, name))
50 return self._req('GET', url_path_join(path, name))
51
51
52 def create_untitled(self, path='/'):
52 def create_untitled(self, path='/'):
53 return self._req('POST', path)
53 return self._req('POST', path)
54
54
55 def upload_untitled(self, body, path='/'):
55 def upload_untitled(self, body, path='/'):
56 return self._req('POST', path, body)
56 return self._req('POST', path, body)
57
57
58 def copy_untitled(self, copy_from, path='/'):
58 def copy_untitled(self, copy_from, path='/'):
59 body = json.dumps({'copy_from':copy_from})
59 body = json.dumps({'copy_from':copy_from})
60 return self._req('POST', path, body)
60 return self._req('POST', path, body)
61
61
62 def create(self, name, path='/'):
62 def create(self, name, path='/'):
63 return self._req('PUT', url_path_join(path, name))
63 return self._req('PUT', url_path_join(path, name))
64
64
65 def upload(self, name, body, path='/'):
65 def upload(self, name, body, path='/'):
66 return self._req('PUT', url_path_join(path, name), body)
66 return self._req('PUT', url_path_join(path, name), body)
67
67
68 def copy(self, copy_from, copy_to, path='/'):
68 def copy(self, copy_from, copy_to, path='/'):
69 body = json.dumps({'copy_from':copy_from})
69 body = json.dumps({'copy_from':copy_from})
70 return self._req('PUT', url_path_join(path, copy_to), body)
70 return self._req('PUT', url_path_join(path, copy_to), body)
71
71
72 def save(self, name, body, path='/'):
72 def save(self, name, body, path='/'):
73 return self._req('PUT', url_path_join(path, name), body)
73 return self._req('PUT', url_path_join(path, name), body)
74
74
75 def delete(self, name, path='/'):
75 def delete(self, name, path='/'):
76 return self._req('DELETE', url_path_join(path, name))
76 return self._req('DELETE', url_path_join(path, name))
77
77
78 def rename(self, name, path, new_name):
78 def rename(self, name, path, new_name):
79 body = json.dumps({'name': new_name})
79 body = json.dumps({'name': new_name})
80 return self._req('PATCH', url_path_join(path, name), body)
80 return self._req('PATCH', url_path_join(path, name), body)
81
81
82 def get_checkpoints(self, name, path):
82 def get_checkpoints(self, name, path):
83 return self._req('GET', url_path_join(path, name, 'checkpoints'))
83 return self._req('GET', url_path_join(path, name, 'checkpoints'))
84
84
85 def new_checkpoint(self, name, path):
85 def new_checkpoint(self, name, path):
86 return self._req('POST', url_path_join(path, name, 'checkpoints'))
86 return self._req('POST', url_path_join(path, name, 'checkpoints'))
87
87
88 def restore_checkpoint(self, name, path, checkpoint_id):
88 def restore_checkpoint(self, name, path, checkpoint_id):
89 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
89 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
90
90
91 def delete_checkpoint(self, name, path, checkpoint_id):
91 def delete_checkpoint(self, name, path, checkpoint_id):
92 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
92 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
93
93
94 class APITest(NotebookTestBase):
94 class APITest(NotebookTestBase):
95 """Test the kernels web service API"""
95 """Test the kernels web service API"""
96 dirs_nbs = [('', 'inroot'),
96 dirs_nbs = [('', 'inroot'),
97 ('Directory with spaces in', 'inspace'),
97 ('Directory with spaces in', 'inspace'),
98 (u'unicodΓ©', 'innonascii'),
98 (u'unicodΓ©', 'innonascii'),
99 ('foo', 'a'),
99 ('foo', 'a'),
100 ('foo', 'b'),
100 ('foo', 'b'),
101 ('foo', 'name with spaces'),
101 ('foo', 'name with spaces'),
102 ('foo', u'unicodΓ©'),
102 ('foo', u'unicodΓ©'),
103 ('foo/bar', 'baz'),
103 ('foo/bar', 'baz'),
104 ('ordering', 'A'),
104 ('ordering', 'A'),
105 ('ordering', 'b'),
105 ('ordering', 'b'),
106 ('ordering', 'C'),
106 ('ordering', 'C'),
107 (u'Γ₯ b', u'Γ§ d'),
107 (u'Γ₯ b', u'Γ§ d'),
108 ]
108 ]
109 hidden_dirs = ['.hidden', '__pycache__']
109 hidden_dirs = ['.hidden', '__pycache__']
110
110
111 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
111 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
112 del dirs[0] # remove ''
112 del dirs[0] # remove ''
113 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
113 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
114
114
115 def setUp(self):
115 def setUp(self):
116 nbdir = self.notebook_dir.name
116 nbdir = self.notebook_dir.name
117
117
118 for d in (self.dirs + self.hidden_dirs):
118 for d in (self.dirs + self.hidden_dirs):
119 d.replace('/', os.sep)
119 d.replace('/', os.sep)
120 if not os.path.isdir(pjoin(nbdir, d)):
120 if not os.path.isdir(pjoin(nbdir, d)):
121 os.mkdir(pjoin(nbdir, d))
121 os.mkdir(pjoin(nbdir, d))
122
122
123 for d, name in self.dirs_nbs:
123 for d, name in self.dirs_nbs:
124 d = d.replace('/', os.sep)
124 d = d.replace('/', os.sep)
125 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
125 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
126 encoding='utf-8') as f:
126 encoding='utf-8') as f:
127 nb = new_notebook(name=name)
127 nb = new_notebook(name=name)
128 write(nb, f, format='ipynb')
128 write(nb, f, format='ipynb')
129
129
130 self.nb_api = NBAPI(self.base_url())
130 self.api = API(self.base_url())
131
131
132 def tearDown(self):
132 def tearDown(self):
133 nbdir = self.notebook_dir.name
133 nbdir = self.notebook_dir.name
134
134
135 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
135 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
136 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
136 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
137
137
138 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
138 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
139 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
139 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
140
140
141 def test_list_notebooks(self):
141 def test_list_notebooks(self):
142 nbs = notebooks_only(self.nb_api.list().json())
142 nbs = notebooks_only(self.api.list().json())
143 self.assertEqual(len(nbs), 1)
143 self.assertEqual(len(nbs), 1)
144 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
144 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
145
145
146 nbs = notebooks_only(self.nb_api.list('/Directory with spaces in/').json())
146 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
147 self.assertEqual(len(nbs), 1)
147 self.assertEqual(len(nbs), 1)
148 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
148 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
149
149
150 nbs = notebooks_only(self.nb_api.list(u'/unicodΓ©/').json())
150 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
151 self.assertEqual(len(nbs), 1)
151 self.assertEqual(len(nbs), 1)
152 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
152 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
153 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
153 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
154
154
155 nbs = notebooks_only(self.nb_api.list('/foo/bar/').json())
155 nbs = notebooks_only(self.api.list('/foo/bar/').json())
156 self.assertEqual(len(nbs), 1)
156 self.assertEqual(len(nbs), 1)
157 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
157 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
158 self.assertEqual(nbs[0]['path'], 'foo/bar')
158 self.assertEqual(nbs[0]['path'], 'foo/bar')
159
159
160 nbs = notebooks_only(self.nb_api.list('foo').json())
160 nbs = notebooks_only(self.api.list('foo').json())
161 self.assertEqual(len(nbs), 4)
161 self.assertEqual(len(nbs), 4)
162 nbnames = { normalize('NFC', n['name']) for n in nbs }
162 nbnames = { normalize('NFC', n['name']) for n in nbs }
163 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
163 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
164 expected = { normalize('NFC', name) for name in expected }
164 expected = { normalize('NFC', name) for name in expected }
165 self.assertEqual(nbnames, expected)
165 self.assertEqual(nbnames, expected)
166
166
167 nbs = notebooks_only(self.nb_api.list('ordering').json())
167 nbs = notebooks_only(self.api.list('ordering').json())
168 nbnames = [n['name'] for n in nbs]
168 nbnames = [n['name'] for n in nbs]
169 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
169 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
170 self.assertEqual(nbnames, expected)
170 self.assertEqual(nbnames, expected)
171
171
172 def test_list_dirs(self):
172 def test_list_dirs(self):
173 dirs = dirs_only(self.nb_api.list().json())
173 dirs = dirs_only(self.api.list().json())
174 dir_names = {normalize('NFC', d['name']) for d in dirs}
174 dir_names = {normalize('NFC', d['name']) for d in dirs}
175 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
175 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
176
176
177 def test_list_nonexistant_dir(self):
177 def test_list_nonexistant_dir(self):
178 with assert_http_error(404):
178 with assert_http_error(404):
179 self.nb_api.list('nonexistant')
179 self.api.list('nonexistant')
180
180
181 def test_get_contents(self):
181 def test_get_contents(self):
182 for d, name in self.dirs_nbs:
182 for d, name in self.dirs_nbs:
183 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
183 nb = self.api.read('%s.ipynb' % name, d+'/').json()
184 self.assertEqual(nb['name'], u'%s.ipynb' % name)
184 self.assertEqual(nb['name'], u'%s.ipynb' % name)
185 self.assertIn('content', nb)
185 self.assertIn('content', nb)
186 self.assertIn('metadata', nb['content'])
186 self.assertIn('metadata', nb['content'])
187 self.assertIsInstance(nb['content']['metadata'], dict)
187 self.assertIsInstance(nb['content']['metadata'], dict)
188
188
189 # Name that doesn't exist - should be a 404
189 # Name that doesn't exist - should be a 404
190 with assert_http_error(404):
190 with assert_http_error(404):
191 self.nb_api.read('q.ipynb', 'foo')
191 self.api.read('q.ipynb', 'foo')
192
192
193 def _check_nb_created(self, resp, name, path):
193 def _check_nb_created(self, resp, name, path):
194 self.assertEqual(resp.status_code, 201)
194 self.assertEqual(resp.status_code, 201)
195 location_header = py3compat.str_to_unicode(resp.headers['Location'])
195 location_header = py3compat.str_to_unicode(resp.headers['Location'])
196 self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
196 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
197 self.assertEqual(resp.json()['name'], name)
197 self.assertEqual(resp.json()['name'], name)
198 assert os.path.isfile(pjoin(
198 assert os.path.isfile(pjoin(
199 self.notebook_dir.name,
199 self.notebook_dir.name,
200 path.replace('/', os.sep),
200 path.replace('/', os.sep),
201 name,
201 name,
202 ))
202 ))
203
203
204 def test_create_untitled(self):
204 def test_create_untitled(self):
205 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
205 resp = self.api.create_untitled(path=u'Γ₯ b')
206 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
206 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
207
207
208 # Second time
208 # Second time
209 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
209 resp = self.api.create_untitled(path=u'Γ₯ b')
210 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
210 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
211
211
212 # And two directories down
212 # And two directories down
213 resp = self.nb_api.create_untitled(path='foo/bar')
213 resp = self.api.create_untitled(path='foo/bar')
214 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
214 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
215
215
216 def test_upload_untitled(self):
216 def test_upload_untitled(self):
217 nb = new_notebook(name='Upload test')
217 nb = new_notebook(name='Upload test')
218 nbmodel = {'content': nb}
218 nbmodel = {'content': nb}
219 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
219 resp = self.api.upload_untitled(path=u'Γ₯ b',
220 body=json.dumps(nbmodel))
220 body=json.dumps(nbmodel))
221 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
221 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
222
222
223 def test_upload(self):
223 def test_upload(self):
224 nb = new_notebook(name=u'ignored')
224 nb = new_notebook(name=u'ignored')
225 nbmodel = {'content': nb}
225 nbmodel = {'content': nb}
226 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
226 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
227 body=json.dumps(nbmodel))
227 body=json.dumps(nbmodel))
228 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
228 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
229
229
230 def test_upload_v2(self):
230 def test_upload_v2(self):
231 nb = v2.new_notebook()
231 nb = v2.new_notebook()
232 ws = v2.new_worksheet()
232 ws = v2.new_worksheet()
233 nb.worksheets.append(ws)
233 nb.worksheets.append(ws)
234 ws.cells.append(v2.new_code_cell(input='print("hi")'))
234 ws.cells.append(v2.new_code_cell(input='print("hi")'))
235 nbmodel = {'content': nb}
235 nbmodel = {'content': nb}
236 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
236 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
237 body=json.dumps(nbmodel))
237 body=json.dumps(nbmodel))
238 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
238 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
239 resp = self.nb_api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
239 resp = self.api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
240 data = resp.json()
240 data = resp.json()
241 self.assertEqual(data['content']['nbformat'], current.nbformat)
241 self.assertEqual(data['content']['nbformat'], current.nbformat)
242 self.assertEqual(data['content']['orig_nbformat'], 2)
242 self.assertEqual(data['content']['orig_nbformat'], 2)
243
243
244 def test_copy_untitled(self):
244 def test_copy_untitled(self):
245 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
245 resp = self.api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
246 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
246 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
247
247
248 def test_copy(self):
248 def test_copy(self):
249 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
249 resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
250 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
250 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
251
251
252 def test_delete(self):
252 def test_delete(self):
253 for d, name in self.dirs_nbs:
253 for d, name in self.dirs_nbs:
254 resp = self.nb_api.delete('%s.ipynb' % name, d)
254 resp = self.api.delete('%s.ipynb' % name, d)
255 self.assertEqual(resp.status_code, 204)
255 self.assertEqual(resp.status_code, 204)
256
256
257 for d in self.dirs + ['/']:
257 for d in self.dirs + ['/']:
258 nbs = notebooks_only(self.nb_api.list(d).json())
258 nbs = notebooks_only(self.api.list(d).json())
259 self.assertEqual(len(nbs), 0)
259 self.assertEqual(len(nbs), 0)
260
260
261 def test_rename(self):
261 def test_rename(self):
262 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
262 resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
263 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
263 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
264 self.assertEqual(resp.json()['name'], 'z.ipynb')
264 self.assertEqual(resp.json()['name'], 'z.ipynb')
265 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
265 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
266
266
267 nbs = notebooks_only(self.nb_api.list('foo').json())
267 nbs = notebooks_only(self.api.list('foo').json())
268 nbnames = set(n['name'] for n in nbs)
268 nbnames = set(n['name'] for n in nbs)
269 self.assertIn('z.ipynb', nbnames)
269 self.assertIn('z.ipynb', nbnames)
270 self.assertNotIn('a.ipynb', nbnames)
270 self.assertNotIn('a.ipynb', nbnames)
271
271
272 def test_rename_existing(self):
272 def test_rename_existing(self):
273 with assert_http_error(409):
273 with assert_http_error(409):
274 self.nb_api.rename('a.ipynb', 'foo', 'b.ipynb')
274 self.api.rename('a.ipynb', 'foo', 'b.ipynb')
275
275
276 def test_save(self):
276 def test_save(self):
277 resp = self.nb_api.read('a.ipynb', 'foo')
277 resp = self.api.read('a.ipynb', 'foo')
278 nbcontent = json.loads(resp.text)['content']
278 nbcontent = json.loads(resp.text)['content']
279 nb = to_notebook_json(nbcontent)
279 nb = to_notebook_json(nbcontent)
280 ws = new_worksheet()
280 ws = new_worksheet()
281 nb.worksheets = [ws]
281 nb.worksheets = [ws]
282 ws.cells.append(new_heading_cell(u'Created by test Β³'))
282 ws.cells.append(new_heading_cell(u'Created by test Β³'))
283
283
284 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
284 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
285 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
285 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
286
286
287 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
287 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
288 with io.open(nbfile, 'r', encoding='utf-8') as f:
288 with io.open(nbfile, 'r', encoding='utf-8') as f:
289 newnb = read(f, format='ipynb')
289 newnb = read(f, format='ipynb')
290 self.assertEqual(newnb.worksheets[0].cells[0].source,
290 self.assertEqual(newnb.worksheets[0].cells[0].source,
291 u'Created by test Β³')
291 u'Created by test Β³')
292 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
292 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
293 newnb = to_notebook_json(nbcontent)
293 newnb = to_notebook_json(nbcontent)
294 self.assertEqual(newnb.worksheets[0].cells[0].source,
294 self.assertEqual(newnb.worksheets[0].cells[0].source,
295 u'Created by test Β³')
295 u'Created by test Β³')
296
296
297 # Save and rename
297 # Save and rename
298 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
298 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
299 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
299 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
300 saved = resp.json()
300 saved = resp.json()
301 self.assertEqual(saved['name'], 'a2.ipynb')
301 self.assertEqual(saved['name'], 'a2.ipynb')
302 self.assertEqual(saved['path'], 'foo/bar')
302 self.assertEqual(saved['path'], 'foo/bar')
303 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
303 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
304 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
304 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
305 with assert_http_error(404):
305 with assert_http_error(404):
306 self.nb_api.read('a.ipynb', 'foo')
306 self.api.read('a.ipynb', 'foo')
307
307
308 def test_checkpoints(self):
308 def test_checkpoints(self):
309 resp = self.nb_api.read('a.ipynb', 'foo')
309 resp = self.api.read('a.ipynb', 'foo')
310 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
310 r = self.api.new_checkpoint('a.ipynb', 'foo')
311 self.assertEqual(r.status_code, 201)
311 self.assertEqual(r.status_code, 201)
312 cp1 = r.json()
312 cp1 = r.json()
313 self.assertEqual(set(cp1), {'id', 'last_modified'})
313 self.assertEqual(set(cp1), {'id', 'last_modified'})
314 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
314 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
315
315
316 # Modify it
316 # Modify it
317 nbcontent = json.loads(resp.text)['content']
317 nbcontent = json.loads(resp.text)['content']
318 nb = to_notebook_json(nbcontent)
318 nb = to_notebook_json(nbcontent)
319 ws = new_worksheet()
319 ws = new_worksheet()
320 nb.worksheets = [ws]
320 nb.worksheets = [ws]
321 hcell = new_heading_cell('Created by test')
321 hcell = new_heading_cell('Created by test')
322 ws.cells.append(hcell)
322 ws.cells.append(hcell)
323 # Save
323 # Save
324 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
324 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
325 resp = self.nb_api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
325 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
326
326
327 # List checkpoints
327 # List checkpoints
328 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
328 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
329 self.assertEqual(cps, [cp1])
329 self.assertEqual(cps, [cp1])
330
330
331 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
331 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
332 nb = to_notebook_json(nbcontent)
332 nb = to_notebook_json(nbcontent)
333 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
333 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
334
334
335 # Restore cp1
335 # Restore cp1
336 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
336 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
337 self.assertEqual(r.status_code, 204)
337 self.assertEqual(r.status_code, 204)
338 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
338 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
339 nb = to_notebook_json(nbcontent)
339 nb = to_notebook_json(nbcontent)
340 self.assertEqual(nb.worksheets, [])
340 self.assertEqual(nb.worksheets, [])
341
341
342 # Delete cp1
342 # Delete cp1
343 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
343 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
344 self.assertEqual(r.status_code, 204)
344 self.assertEqual(r.status_code, 204)
345 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
345 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
346 self.assertEqual(cps, [])
346 self.assertEqual(cps, [])
@@ -1,320 +1,301 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import logging
5 import logging
6 import os
6 import os
7
7
8 from tornado.web import HTTPError
8 from tornado.web import HTTPError
9 from unittest import TestCase
9 from unittest import TestCase
10 from tempfile import NamedTemporaryFile
10 from tempfile import NamedTemporaryFile
11
11
12 from IPython.nbformat import current
12 from IPython.nbformat import current
13
13
14 from IPython.utils.tempdir import TemporaryDirectory
14 from IPython.utils.tempdir import TemporaryDirectory
15 from IPython.utils.traitlets import TraitError
15 from IPython.utils.traitlets import TraitError
16 from IPython.html.utils import url_path_join
16 from IPython.html.utils import url_path_join
17
17
18 from ..filenbmanager import FileNotebookManager
18 from ..filemanager import FileContentsManager
19 from ..nbmanager import NotebookManager
19 from ..manager import ContentsManager
20
20
21
21
22 class TestFileNotebookManager(TestCase):
22 class TestFileContentsManager(TestCase):
23
23
24 def test_nb_dir(self):
24 def test_root_dir(self):
25 with TemporaryDirectory() as td:
25 with TemporaryDirectory() as td:
26 fm = FileNotebookManager(notebook_dir=td)
26 fm = FileContentsManager(root_dir=td)
27 self.assertEqual(fm.notebook_dir, td)
27 self.assertEqual(fm.root_dir, td)
28
28
29 def test_missing_nb_dir(self):
29 def test_missing_root_dir(self):
30 with TemporaryDirectory() as td:
30 with TemporaryDirectory() as td:
31 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
31 root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
32 self.assertRaises(TraitError, FileContentsManager, root_dir=root)
33
33
34 def test_invalid_nb_dir(self):
34 def test_invalid_root_dir(self):
35 with NamedTemporaryFile() as tf:
35 with NamedTemporaryFile() as tf:
36 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
36 self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
37
37
38 def test_get_os_path(self):
38 def test_get_os_path(self):
39 # full filesystem path should be returned with correct operating system
39 # full filesystem path should be returned with correct operating system
40 # separators.
40 # separators.
41 with TemporaryDirectory() as td:
41 with TemporaryDirectory() as td:
42 nbdir = td
42 root = td
43 fm = FileNotebookManager(notebook_dir=nbdir)
43 fm = FileContentsManager(root_dir=root)
44 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
44 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
46 fs_path = os.path.join(fm.root_dir, *rel_path_list)
47 self.assertEqual(path, fs_path)
47 self.assertEqual(path, fs_path)
48
48
49 fm = FileNotebookManager(notebook_dir=nbdir)
49 fm = FileContentsManager(root_dir=root)
50 path = fm._get_os_path('test.ipynb')
50 path = fm._get_os_path('test.ipynb')
51 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
51 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
52 self.assertEqual(path, fs_path)
52 self.assertEqual(path, fs_path)
53
53
54 fm = FileNotebookManager(notebook_dir=nbdir)
54 fm = FileContentsManager(root_dir=root)
55 path = fm._get_os_path('test.ipynb', '////')
55 path = fm._get_os_path('test.ipynb', '////')
56 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
56 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
57 self.assertEqual(path, fs_path)
57 self.assertEqual(path, fs_path)
58
58
59 def test_checkpoint_subdir(self):
59 def test_checkpoint_subdir(self):
60 subd = u'sub βˆ‚ir'
60 subd = u'sub βˆ‚ir'
61 cp_name = 'test-cp.ipynb'
61 cp_name = 'test-cp.ipynb'
62 with TemporaryDirectory() as td:
62 with TemporaryDirectory() as td:
63 nbdir = td
63 root = td
64 os.mkdir(os.path.join(td, subd))
64 os.mkdir(os.path.join(td, subd))
65 fm = FileNotebookManager(notebook_dir=nbdir)
65 fm = FileContentsManager(root_dir=root)
66 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
66 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
67 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 self.assertNotEqual(cp_dir, cp_subdir)
68 self.assertNotEqual(cp_dir, cp_subdir)
69 self.assertEqual(cp_dir, os.path.join(nbdir, fm.checkpoint_dir, cp_name))
69 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_subdir, os.path.join(nbdir, subd, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
71
71
72
72
73 class TestNotebookManager(TestCase):
73 class TestNotebookManager(TestCase):
74
74
75 def setUp(self):
75 def setUp(self):
76 self._temp_dir = TemporaryDirectory()
76 self._temp_dir = TemporaryDirectory()
77 self.td = self._temp_dir.name
77 self.td = self._temp_dir.name
78 self.notebook_manager = FileNotebookManager(
78 self.contents_manager = FileContentsManager(
79 notebook_dir=self.td,
79 root_dir=self.td,
80 log=logging.getLogger()
80 log=logging.getLogger()
81 )
81 )
82
82
83 def tearDown(self):
83 def tearDown(self):
84 self._temp_dir.cleanup()
84 self._temp_dir.cleanup()
85
85
86 def make_dir(self, abs_path, rel_path):
86 def make_dir(self, abs_path, rel_path):
87 """make subdirectory, rel_path is the relative path
87 """make subdirectory, rel_path is the relative path
88 to that directory from the location where the server started"""
88 to that directory from the location where the server started"""
89 os_path = os.path.join(abs_path, rel_path)
89 os_path = os.path.join(abs_path, rel_path)
90 try:
90 try:
91 os.makedirs(os_path)
91 os.makedirs(os_path)
92 except OSError:
92 except OSError:
93 print("Directory already exists: %r" % os_path)
93 print("Directory already exists: %r" % os_path)
94
94
95 def add_code_cell(self, nb):
95 def add_code_cell(self, nb):
96 output = current.new_output("display_data", output_javascript="alert('hi');")
96 output = current.new_output("display_data", output_javascript="alert('hi');")
97 cell = current.new_code_cell("print('hi')", outputs=[output])
97 cell = current.new_code_cell("print('hi')", outputs=[output])
98 if not nb.worksheets:
98 if not nb.worksheets:
99 nb.worksheets.append(current.new_worksheet())
99 nb.worksheets.append(current.new_worksheet())
100 nb.worksheets[0].cells.append(cell)
100 nb.worksheets[0].cells.append(cell)
101
101
102 def new_notebook(self):
102 def new_notebook(self):
103 nbm = self.notebook_manager
103 cm = self.contents_manager
104 model = nbm.create_notebook()
104 model = cm.create_notebook()
105 name = model['name']
105 name = model['name']
106 path = model['path']
106 path = model['path']
107
107
108 full_model = nbm.get_notebook(name, path)
108 full_model = cm.get(name, path)
109 nb = full_model['content']
109 nb = full_model['content']
110 self.add_code_cell(nb)
110 self.add_code_cell(nb)
111
111
112 nbm.save_notebook(full_model, name, path)
112 cm.save(full_model, name, path)
113 return nb, name, path
113 return nb, name, path
114
114
115 def test_create_notebook(self):
115 def test_create_notebook(self):
116 nm = self.notebook_manager
116 cm = self.contents_manager
117 # Test in root directory
117 # Test in root directory
118 model = nm.create_notebook()
118 model = cm.create_notebook()
119 assert isinstance(model, dict)
119 assert isinstance(model, dict)
120 self.assertIn('name', model)
120 self.assertIn('name', model)
121 self.assertIn('path', model)
121 self.assertIn('path', model)
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
123 self.assertEqual(model['path'], '')
123 self.assertEqual(model['path'], '')
124
124
125 # Test in sub-directory
125 # Test in sub-directory
126 sub_dir = '/foo/'
126 sub_dir = '/foo/'
127 self.make_dir(nm.notebook_dir, 'foo')
127 self.make_dir(cm.root_dir, 'foo')
128 model = nm.create_notebook(None, sub_dir)
128 model = cm.create_notebook(None, sub_dir)
129 assert isinstance(model, dict)
129 assert isinstance(model, dict)
130 self.assertIn('name', model)
130 self.assertIn('name', model)
131 self.assertIn('path', model)
131 self.assertIn('path', model)
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
133 self.assertEqual(model['path'], sub_dir.strip('/'))
133 self.assertEqual(model['path'], sub_dir.strip('/'))
134
134
135 def test_get_notebook(self):
135 def test_get(self):
136 nm = self.notebook_manager
136 cm = self.contents_manager
137 # Create a notebook
137 # Create a notebook
138 model = nm.create_notebook()
138 model = cm.create_notebook()
139 name = model['name']
139 name = model['name']
140 path = model['path']
140 path = model['path']
141
141
142 # Check that we 'get' on the notebook we just created
142 # Check that we 'get' on the notebook we just created
143 model2 = nm.get_notebook(name, path)
143 model2 = cm.get(name, path)
144 assert isinstance(model2, dict)
144 assert isinstance(model2, dict)
145 self.assertIn('name', model2)
145 self.assertIn('name', model2)
146 self.assertIn('path', model2)
146 self.assertIn('path', model2)
147 self.assertEqual(model['name'], name)
147 self.assertEqual(model['name'], name)
148 self.assertEqual(model['path'], path)
148 self.assertEqual(model['path'], path)
149
149
150 # Test in sub-directory
150 # Test in sub-directory
151 sub_dir = '/foo/'
151 sub_dir = '/foo/'
152 self.make_dir(nm.notebook_dir, 'foo')
152 self.make_dir(cm.root_dir, 'foo')
153 model = nm.create_notebook(None, sub_dir)
153 model = cm.create_notebook(None, sub_dir)
154 model2 = nm.get_notebook(name, sub_dir)
154 model2 = cm.get(name, sub_dir)
155 assert isinstance(model2, dict)
155 assert isinstance(model2, dict)
156 self.assertIn('name', model2)
156 self.assertIn('name', model2)
157 self.assertIn('path', model2)
157 self.assertIn('path', model2)
158 self.assertIn('content', model2)
158 self.assertIn('content', model2)
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
160 self.assertEqual(model2['path'], sub_dir.strip('/'))
160 self.assertEqual(model2['path'], sub_dir.strip('/'))
161
161
162 def test_update_notebook(self):
162 def test_update(self):
163 nm = self.notebook_manager
163 cm = self.contents_manager
164 # Create a notebook
164 # Create a notebook
165 model = nm.create_notebook()
165 model = cm.create_notebook()
166 name = model['name']
166 name = model['name']
167 path = model['path']
167 path = model['path']
168
168
169 # Change the name in the model for rename
169 # Change the name in the model for rename
170 model['name'] = 'test.ipynb'
170 model['name'] = 'test.ipynb'
171 model = nm.update_notebook(model, name, path)
171 model = cm.update(model, name, path)
172 assert isinstance(model, dict)
172 assert isinstance(model, dict)
173 self.assertIn('name', model)
173 self.assertIn('name', model)
174 self.assertIn('path', model)
174 self.assertIn('path', model)
175 self.assertEqual(model['name'], 'test.ipynb')
175 self.assertEqual(model['name'], 'test.ipynb')
176
176
177 # Make sure the old name is gone
177 # Make sure the old name is gone
178 self.assertRaises(HTTPError, nm.get_notebook, name, path)
178 self.assertRaises(HTTPError, cm.get, name, path)
179
179
180 # Test in sub-directory
180 # Test in sub-directory
181 # Create a directory and notebook in that directory
181 # Create a directory and notebook in that directory
182 sub_dir = '/foo/'
182 sub_dir = '/foo/'
183 self.make_dir(nm.notebook_dir, 'foo')
183 self.make_dir(cm.root_dir, 'foo')
184 model = nm.create_notebook(None, sub_dir)
184 model = cm.create_notebook(None, sub_dir)
185 name = model['name']
185 name = model['name']
186 path = model['path']
186 path = model['path']
187
187
188 # Change the name in the model for rename
188 # Change the name in the model for rename
189 model['name'] = 'test_in_sub.ipynb'
189 model['name'] = 'test_in_sub.ipynb'
190 model = nm.update_notebook(model, name, path)
190 model = cm.update(model, name, path)
191 assert isinstance(model, dict)
191 assert isinstance(model, dict)
192 self.assertIn('name', model)
192 self.assertIn('name', model)
193 self.assertIn('path', model)
193 self.assertIn('path', model)
194 self.assertEqual(model['name'], 'test_in_sub.ipynb')
194 self.assertEqual(model['name'], 'test_in_sub.ipynb')
195 self.assertEqual(model['path'], sub_dir.strip('/'))
195 self.assertEqual(model['path'], sub_dir.strip('/'))
196
196
197 # Make sure the old name is gone
197 # Make sure the old name is gone
198 self.assertRaises(HTTPError, nm.get_notebook, name, path)
198 self.assertRaises(HTTPError, cm.get, name, path)
199
199
200 def test_save_notebook(self):
200 def test_save(self):
201 nm = self.notebook_manager
201 cm = self.contents_manager
202 # Create a notebook
202 # Create a notebook
203 model = nm.create_notebook()
203 model = cm.create_notebook()
204 name = model['name']
204 name = model['name']
205 path = model['path']
205 path = model['path']
206
206
207 # Get the model with 'content'
207 # Get the model with 'content'
208 full_model = nm.get_notebook(name, path)
208 full_model = cm.get(name, path)
209
209
210 # Save the notebook
210 # Save the notebook
211 model = nm.save_notebook(full_model, name, path)
211 model = cm.save(full_model, name, path)
212 assert isinstance(model, dict)
212 assert isinstance(model, dict)
213 self.assertIn('name', model)
213 self.assertIn('name', model)
214 self.assertIn('path', model)
214 self.assertIn('path', model)
215 self.assertEqual(model['name'], name)
215 self.assertEqual(model['name'], name)
216 self.assertEqual(model['path'], path)
216 self.assertEqual(model['path'], path)
217
217
218 # Test in sub-directory
218 # Test in sub-directory
219 # Create a directory and notebook in that directory
219 # Create a directory and notebook in that directory
220 sub_dir = '/foo/'
220 sub_dir = '/foo/'
221 self.make_dir(nm.notebook_dir, 'foo')
221 self.make_dir(cm.root_dir, 'foo')
222 model = nm.create_notebook(None, sub_dir)
222 model = cm.create_notebook(None, sub_dir)
223 name = model['name']
223 name = model['name']
224 path = model['path']
224 path = model['path']
225 model = nm.get_notebook(name, path)
225 model = cm.get(name, path)
226
226
227 # Change the name in the model for rename
227 # Change the name in the model for rename
228 model = nm.save_notebook(model, name, path)
228 model = cm.save(model, name, path)
229 assert isinstance(model, dict)
229 assert isinstance(model, dict)
230 self.assertIn('name', model)
230 self.assertIn('name', model)
231 self.assertIn('path', model)
231 self.assertIn('path', model)
232 self.assertEqual(model['name'], 'Untitled0.ipynb')
232 self.assertEqual(model['name'], 'Untitled0.ipynb')
233 self.assertEqual(model['path'], sub_dir.strip('/'))
233 self.assertEqual(model['path'], sub_dir.strip('/'))
234
234
235 def test_save_notebook_with_script(self):
235 def test_delete(self):
236 nm = self.notebook_manager
236 cm = self.contents_manager
237 # Create a notebook
238 model = nm.create_notebook()
239 nm.save_script = True
240 model = nm.create_notebook()
241 name = model['name']
242 path = model['path']
243
244 # Get the model with 'content'
245 full_model = nm.get_notebook(name, path)
246
247 # Save the notebook
248 model = nm.save_notebook(full_model, name, path)
249
250 # Check that the script was created
251 py_path = os.path.join(nm.notebook_dir, os.path.splitext(name)[0]+'.py')
252 assert os.path.exists(py_path), py_path
253
254 def test_delete_notebook(self):
255 nm = self.notebook_manager
256 # Create a notebook
237 # Create a notebook
257 nb, name, path = self.new_notebook()
238 nb, name, path = self.new_notebook()
258
239
259 # Delete the notebook
240 # Delete the notebook
260 nm.delete_notebook(name, path)
241 cm.delete(name, path)
261
242
262 # Check that a 'get' on the deleted notebook raises and error
243 # Check that a 'get' on the deleted notebook raises and error
263 self.assertRaises(HTTPError, nm.get_notebook, name, path)
244 self.assertRaises(HTTPError, cm.get, name, path)
264
245
265 def test_copy_notebook(self):
246 def test_copy(self):
266 nm = self.notebook_manager
247 cm = self.contents_manager
267 path = u'Γ₯ b'
248 path = u'Γ₯ b'
268 name = u'nb √.ipynb'
249 name = u'nb √.ipynb'
269 os.mkdir(os.path.join(nm.notebook_dir, path))
250 os.mkdir(os.path.join(cm.root_dir, path))
270 orig = nm.create_notebook({'name' : name}, path=path)
251 orig = cm.create_notebook({'name' : name}, path=path)
271
252
272 # copy with unspecified name
253 # copy with unspecified name
273 copy = nm.copy_notebook(name, path=path)
254 copy = cm.copy(name, path=path)
274 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
255 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
275
256
276 # copy with specified name
257 # copy with specified name
277 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
258 copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
278 self.assertEqual(copy2['name'], u'copy 2.ipynb')
259 self.assertEqual(copy2['name'], u'copy 2.ipynb')
279
260
280 def test_trust_notebook(self):
261 def test_trust_notebook(self):
281 nbm = self.notebook_manager
262 cm = self.contents_manager
282 nb, name, path = self.new_notebook()
263 nb, name, path = self.new_notebook()
283
264
284 untrusted = nbm.get_notebook(name, path)['content']
265 untrusted = cm.get(name, path)['content']
285 assert not nbm.notary.check_cells(untrusted)
266 assert not cm.notary.check_cells(untrusted)
286
267
287 # print(untrusted)
268 # print(untrusted)
288 nbm.trust_notebook(name, path)
269 cm.trust_notebook(name, path)
289 trusted = nbm.get_notebook(name, path)['content']
270 trusted = cm.get(name, path)['content']
290 # print(trusted)
271 # print(trusted)
291 assert nbm.notary.check_cells(trusted)
272 assert cm.notary.check_cells(trusted)
292
273
293 def test_mark_trusted_cells(self):
274 def test_mark_trusted_cells(self):
294 nbm = self.notebook_manager
275 cm = self.contents_manager
295 nb, name, path = self.new_notebook()
276 nb, name, path = self.new_notebook()
296
277
297 nbm.mark_trusted_cells(nb, name, path)
278 cm.mark_trusted_cells(nb, name, path)
298 for cell in nb.worksheets[0].cells:
279 for cell in nb.worksheets[0].cells:
299 if cell.cell_type == 'code':
280 if cell.cell_type == 'code':
300 assert not cell.trusted
281 assert not cell.trusted
301
282
302 nbm.trust_notebook(name, path)
283 cm.trust_notebook(name, path)
303 nb = nbm.get_notebook(name, path)['content']
284 nb = cm.get(name, path)['content']
304 for cell in nb.worksheets[0].cells:
285 for cell in nb.worksheets[0].cells:
305 if cell.cell_type == 'code':
286 if cell.cell_type == 'code':
306 assert cell.trusted
287 assert cell.trusted
307
288
308 def test_check_and_sign(self):
289 def test_check_and_sign(self):
309 nbm = self.notebook_manager
290 cm = self.contents_manager
310 nb, name, path = self.new_notebook()
291 nb, name, path = self.new_notebook()
311
292
312 nbm.mark_trusted_cells(nb, name, path)
293 cm.mark_trusted_cells(nb, name, path)
313 nbm.check_and_sign(nb, name, path)
294 cm.check_and_sign(nb, name, path)
314 assert not nbm.notary.check_signature(nb)
295 assert not cm.notary.check_signature(nb)
315
296
316 nbm.trust_notebook(name, path)
297 cm.trust_notebook(name, path)
317 nb = nbm.get_notebook(name, path)['content']
298 nb = cm.get(name, path)['content']
318 nbm.mark_trusted_cells(nb, name, path)
299 cm.mark_trusted_cells(nb, name, path)
319 nbm.check_and_sign(nb, name, path)
300 cm.check_and_sign(nb, name, path)
320 assert nbm.notary.check_signature(nb)
301 assert cm.notary.check_signature(nb)
@@ -1,127 +1,112 b''
1 """Tornado handlers for the sessions web service.
1 """Tornado handlers for the sessions web service."""
2
2
3 Authors:
3 # Copyright (c) IPython Development Team.
4
4 # Distributed under the terms of the Modified BSD License.
5 * Zach Sailer
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
5
19 import json
6 import json
20
7
21 from tornado import web
8 from tornado import web
22
9
23 from ...base.handlers import IPythonHandler, json_errors
10 from ...base.handlers import IPythonHandler, json_errors
24 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join, url_escape
12 from IPython.html.utils import url_path_join, url_escape
26
13
27 #-----------------------------------------------------------------------------
28 # Session web service handlers
29 #-----------------------------------------------------------------------------
30
31
14
32 class SessionRootHandler(IPythonHandler):
15 class SessionRootHandler(IPythonHandler):
33
16
34 @web.authenticated
17 @web.authenticated
35 @json_errors
18 @json_errors
36 def get(self):
19 def get(self):
37 # Return a list of running sessions
20 # Return a list of running sessions
38 sm = self.session_manager
21 sm = self.session_manager
39 sessions = sm.list_sessions()
22 sessions = sm.list_sessions()
40 self.finish(json.dumps(sessions, default=date_default))
23 self.finish(json.dumps(sessions, default=date_default))
41
24
42 @web.authenticated
25 @web.authenticated
43 @json_errors
26 @json_errors
44 def post(self):
27 def post(self):
45 # Creates a new session
28 # Creates a new session
46 #(unless a session already exists for the named nb)
29 #(unless a session already exists for the named nb)
47 sm = self.session_manager
30 sm = self.session_manager
31 cm = self.contents_manager
32 km = self.kernel_manager
48
33
49 model = self.get_json_body()
34 model = self.get_json_body()
50 if model is None:
35 if model is None:
51 raise web.HTTPError(400, "No JSON data provided")
36 raise web.HTTPError(400, "No JSON data provided")
52 try:
37 try:
53 name = model['notebook']['name']
38 name = model['notebook']['name']
54 except KeyError:
39 except KeyError:
55 raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
40 raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
56 try:
41 try:
57 path = model['notebook']['path']
42 path = model['notebook']['path']
58 except KeyError:
43 except KeyError:
59 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
44 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
60 try:
45 try:
61 kernel_name = model['kernel']['name']
46 kernel_name = model['kernel']['name']
62 except KeyError:
47 except KeyError:
63 raise web.HTTPError(400, "Missing field in JSON data: kernel.name")
48 raise web.HTTPError(400, "Missing field in JSON data: kernel.name")
64
49
65 # Check to see if session exists
50 # Check to see if session exists
66 if sm.session_exists(name=name, path=path):
51 if sm.session_exists(name=name, path=path):
67 model = sm.get_session(name=name, path=path)
52 model = sm.get_session(name=name, path=path)
68 else:
53 else:
69 model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
54 model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
70 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
55 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
71 self.set_header('Location', url_escape(location))
56 self.set_header('Location', url_escape(location))
72 self.set_status(201)
57 self.set_status(201)
73 self.finish(json.dumps(model, default=date_default))
58 self.finish(json.dumps(model, default=date_default))
74
59
75 class SessionHandler(IPythonHandler):
60 class SessionHandler(IPythonHandler):
76
61
77 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
62 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
78
63
79 @web.authenticated
64 @web.authenticated
80 @json_errors
65 @json_errors
81 def get(self, session_id):
66 def get(self, session_id):
82 # Returns the JSON model for a single session
67 # Returns the JSON model for a single session
83 sm = self.session_manager
68 sm = self.session_manager
84 model = sm.get_session(session_id=session_id)
69 model = sm.get_session(session_id=session_id)
85 self.finish(json.dumps(model, default=date_default))
70 self.finish(json.dumps(model, default=date_default))
86
71
87 @web.authenticated
72 @web.authenticated
88 @json_errors
73 @json_errors
89 def patch(self, session_id):
74 def patch(self, session_id):
90 # Currently, this handler is strictly for renaming notebooks
75 # Currently, this handler is strictly for renaming notebooks
91 sm = self.session_manager
76 sm = self.session_manager
92 model = self.get_json_body()
77 model = self.get_json_body()
93 if model is None:
78 if model is None:
94 raise web.HTTPError(400, "No JSON data provided")
79 raise web.HTTPError(400, "No JSON data provided")
95 changes = {}
80 changes = {}
96 if 'notebook' in model:
81 if 'notebook' in model:
97 notebook = model['notebook']
82 notebook = model['notebook']
98 if 'name' in notebook:
83 if 'name' in notebook:
99 changes['name'] = notebook['name']
84 changes['name'] = notebook['name']
100 if 'path' in notebook:
85 if 'path' in notebook:
101 changes['path'] = notebook['path']
86 changes['path'] = notebook['path']
102
87
103 sm.update_session(session_id, **changes)
88 sm.update_session(session_id, **changes)
104 model = sm.get_session(session_id=session_id)
89 model = sm.get_session(session_id=session_id)
105 self.finish(json.dumps(model, default=date_default))
90 self.finish(json.dumps(model, default=date_default))
106
91
107 @web.authenticated
92 @web.authenticated
108 @json_errors
93 @json_errors
109 def delete(self, session_id):
94 def delete(self, session_id):
110 # Deletes the session with given session_id
95 # Deletes the session with given session_id
111 sm = self.session_manager
96 sm = self.session_manager
112 sm.delete_session(session_id)
97 sm.delete_session(session_id)
113 self.set_status(204)
98 self.set_status(204)
114 self.finish()
99 self.finish()
115
100
116
101
117 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
118 # URL to handler mappings
103 # URL to handler mappings
119 #-----------------------------------------------------------------------------
104 #-----------------------------------------------------------------------------
120
105
121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
106 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
122
107
123 default_handlers = [
108 default_handlers = [
124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
109 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
125 (r"/api/sessions", SessionRootHandler)
110 (r"/api/sessions", SessionRootHandler)
126 ]
111 ]
127
112
@@ -1,206 +1,206 b''
1 """A base class session manager.
1 """A base class session manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Zach Sailer
5 * Zach Sailer
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 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 uuid
19 import uuid
20 import sqlite3
20 import sqlite3
21
21
22 from tornado import web
22 from tornado import web
23
23
24 from IPython.config.configurable import LoggingConfigurable
24 from IPython.config.configurable import LoggingConfigurable
25 from IPython.utils.py3compat import unicode_type
25 from IPython.utils.py3compat import unicode_type
26 from IPython.utils.traitlets import Instance
26 from IPython.utils.traitlets import Instance
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Classes
29 # Classes
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 class SessionManager(LoggingConfigurable):
32 class SessionManager(LoggingConfigurable):
33
33
34 kernel_manager = Instance('IPython.html.services.kernels.kernelmanager.MappingKernelManager')
34 kernel_manager = Instance('IPython.html.services.kernels.kernelmanager.MappingKernelManager')
35 notebook_manager = Instance('IPython.html.services.notebooks.nbmanager.NotebookManager', args=())
35 contents_manager = Instance('IPython.html.services.contents.manager.ContentsManager', args=())
36
36
37 # Session database initialized below
37 # Session database initialized below
38 _cursor = None
38 _cursor = None
39 _connection = None
39 _connection = None
40 _columns = {'session_id', 'name', 'path', 'kernel_id'}
40 _columns = {'session_id', 'name', 'path', 'kernel_id'}
41
41
42 @property
42 @property
43 def cursor(self):
43 def cursor(self):
44 """Start a cursor and create a database called 'session'"""
44 """Start a cursor and create a database called 'session'"""
45 if self._cursor is None:
45 if self._cursor is None:
46 self._cursor = self.connection.cursor()
46 self._cursor = self.connection.cursor()
47 self._cursor.execute("""CREATE TABLE session
47 self._cursor.execute("""CREATE TABLE session
48 (session_id, name, path, kernel_id)""")
48 (session_id, name, path, kernel_id)""")
49 return self._cursor
49 return self._cursor
50
50
51 @property
51 @property
52 def connection(self):
52 def connection(self):
53 """Start a database connection"""
53 """Start a database connection"""
54 if self._connection is None:
54 if self._connection is None:
55 self._connection = sqlite3.connect(':memory:')
55 self._connection = sqlite3.connect(':memory:')
56 self._connection.row_factory = self.row_factory
56 self._connection.row_factory = self.row_factory
57 return self._connection
57 return self._connection
58
58
59 def __del__(self):
59 def __del__(self):
60 """Close connection once SessionManager closes"""
60 """Close connection once SessionManager closes"""
61 self.cursor.close()
61 self.cursor.close()
62
62
63 def session_exists(self, name, path):
63 def session_exists(self, name, path):
64 """Check to see if the session for a given notebook exists"""
64 """Check to see if the session for a given notebook exists"""
65 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
65 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
66 reply = self.cursor.fetchone()
66 reply = self.cursor.fetchone()
67 if reply is None:
67 if reply is None:
68 return False
68 return False
69 else:
69 else:
70 return True
70 return True
71
71
72 def new_session_id(self):
72 def new_session_id(self):
73 "Create a uuid for a new session"
73 "Create a uuid for a new session"
74 return unicode_type(uuid.uuid4())
74 return unicode_type(uuid.uuid4())
75
75
76 def create_session(self, name=None, path=None, kernel_name='python'):
76 def create_session(self, name=None, path=None, kernel_name='python'):
77 """Creates a session and returns its model"""
77 """Creates a session and returns its model"""
78 session_id = self.new_session_id()
78 session_id = self.new_session_id()
79 # allow nbm to specify kernels cwd
79 # allow nbm to specify kernels cwd
80 kernel_path = self.notebook_manager.get_kernel_path(name=name, path=path)
80 kernel_path = self.contents_manager.get_kernel_path(name=name, path=path)
81 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
81 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
82 kernel_name=kernel_name)
82 kernel_name=kernel_name)
83 return self.save_session(session_id, name=name, path=path,
83 return self.save_session(session_id, name=name, path=path,
84 kernel_id=kernel_id)
84 kernel_id=kernel_id)
85
85
86 def save_session(self, session_id, name=None, path=None, kernel_id=None):
86 def save_session(self, session_id, name=None, path=None, kernel_id=None):
87 """Saves the items for the session with the given session_id
87 """Saves the items for the session with the given session_id
88
88
89 Given a session_id (and any other of the arguments), this method
89 Given a session_id (and any other of the arguments), this method
90 creates a row in the sqlite session database that holds the information
90 creates a row in the sqlite session database that holds the information
91 for a session.
91 for a session.
92
92
93 Parameters
93 Parameters
94 ----------
94 ----------
95 session_id : str
95 session_id : str
96 uuid for the session; this method must be given a session_id
96 uuid for the session; this method must be given a session_id
97 name : str
97 name : str
98 the .ipynb notebook name that started the session
98 the .ipynb notebook name that started the session
99 path : str
99 path : str
100 the path to the named notebook
100 the path to the named notebook
101 kernel_id : str
101 kernel_id : str
102 a uuid for the kernel associated with this session
102 a uuid for the kernel associated with this session
103
103
104 Returns
104 Returns
105 -------
105 -------
106 model : dict
106 model : dict
107 a dictionary of the session model
107 a dictionary of the session model
108 """
108 """
109 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
109 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
110 (session_id, name, path, kernel_id)
110 (session_id, name, path, kernel_id)
111 )
111 )
112 return self.get_session(session_id=session_id)
112 return self.get_session(session_id=session_id)
113
113
114 def get_session(self, **kwargs):
114 def get_session(self, **kwargs):
115 """Returns the model for a particular session.
115 """Returns the model for a particular session.
116
116
117 Takes a keyword argument and searches for the value in the session
117 Takes a keyword argument and searches for the value in the session
118 database, then returns the rest of the session's info.
118 database, then returns the rest of the session's info.
119
119
120 Parameters
120 Parameters
121 ----------
121 ----------
122 **kwargs : keyword argument
122 **kwargs : keyword argument
123 must be given one of the keywords and values from the session database
123 must be given one of the keywords and values from the session database
124 (i.e. session_id, name, path, kernel_id)
124 (i.e. session_id, name, path, kernel_id)
125
125
126 Returns
126 Returns
127 -------
127 -------
128 model : dict
128 model : dict
129 returns a dictionary that includes all the information from the
129 returns a dictionary that includes all the information from the
130 session described by the kwarg.
130 session described by the kwarg.
131 """
131 """
132 if not kwargs:
132 if not kwargs:
133 raise TypeError("must specify a column to query")
133 raise TypeError("must specify a column to query")
134
134
135 conditions = []
135 conditions = []
136 for column in kwargs.keys():
136 for column in kwargs.keys():
137 if column not in self._columns:
137 if column not in self._columns:
138 raise TypeError("No such column: %r", column)
138 raise TypeError("No such column: %r", column)
139 conditions.append("%s=?" % column)
139 conditions.append("%s=?" % column)
140
140
141 query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
141 query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
142
142
143 self.cursor.execute(query, list(kwargs.values()))
143 self.cursor.execute(query, list(kwargs.values()))
144 model = self.cursor.fetchone()
144 model = self.cursor.fetchone()
145 if model is None:
145 if model is None:
146 q = []
146 q = []
147 for key, value in kwargs.items():
147 for key, value in kwargs.items():
148 q.append("%s=%r" % (key, value))
148 q.append("%s=%r" % (key, value))
149
149
150 raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
150 raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
151 return model
151 return model
152
152
153 def update_session(self, session_id, **kwargs):
153 def update_session(self, session_id, **kwargs):
154 """Updates the values in the session database.
154 """Updates the values in the session database.
155
155
156 Changes the values of the session with the given session_id
156 Changes the values of the session with the given session_id
157 with the values from the keyword arguments.
157 with the values from the keyword arguments.
158
158
159 Parameters
159 Parameters
160 ----------
160 ----------
161 session_id : str
161 session_id : str
162 a uuid that identifies a session in the sqlite3 database
162 a uuid that identifies a session in the sqlite3 database
163 **kwargs : str
163 **kwargs : str
164 the key must correspond to a column title in session database,
164 the key must correspond to a column title in session database,
165 and the value replaces the current value in the session
165 and the value replaces the current value in the session
166 with session_id.
166 with session_id.
167 """
167 """
168 self.get_session(session_id=session_id)
168 self.get_session(session_id=session_id)
169
169
170 if not kwargs:
170 if not kwargs:
171 # no changes
171 # no changes
172 return
172 return
173
173
174 sets = []
174 sets = []
175 for column in kwargs.keys():
175 for column in kwargs.keys():
176 if column not in self._columns:
176 if column not in self._columns:
177 raise TypeError("No such column: %r" % column)
177 raise TypeError("No such column: %r" % column)
178 sets.append("%s=?" % column)
178 sets.append("%s=?" % column)
179 query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
179 query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
180 self.cursor.execute(query, list(kwargs.values()) + [session_id])
180 self.cursor.execute(query, list(kwargs.values()) + [session_id])
181
181
182 def row_factory(self, cursor, row):
182 def row_factory(self, cursor, row):
183 """Takes sqlite database session row and turns it into a dictionary"""
183 """Takes sqlite database session row and turns it into a dictionary"""
184 row = sqlite3.Row(cursor, row)
184 row = sqlite3.Row(cursor, row)
185 model = {
185 model = {
186 'id': row['session_id'],
186 'id': row['session_id'],
187 'notebook': {
187 'notebook': {
188 'name': row['name'],
188 'name': row['name'],
189 'path': row['path']
189 'path': row['path']
190 },
190 },
191 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
191 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
192 }
192 }
193 return model
193 return model
194
194
195 def list_sessions(self):
195 def list_sessions(self):
196 """Returns a list of dictionaries containing all the information from
196 """Returns a list of dictionaries containing all the information from
197 the session database"""
197 the session database"""
198 c = self.cursor.execute("SELECT * FROM session")
198 c = self.cursor.execute("SELECT * FROM session")
199 return list(c.fetchall())
199 return list(c.fetchall())
200
200
201 def delete_session(self, session_id):
201 def delete_session(self, session_id):
202 """Deletes the row in the session database with given session_id"""
202 """Deletes the row in the session database with given session_id"""
203 # Check that session exists before deleting
203 # Check that session exists before deleting
204 session = self.get_session(session_id=session_id)
204 session = self.get_session(session_id=session_id)
205 self.kernel_manager.shutdown_kernel(session['kernel']['id'])
205 self.kernel_manager.shutdown_kernel(session['kernel']['id'])
206 self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
206 self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
@@ -1,2579 +1,2579 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 'notebook/js/textcell',
9 'notebook/js/textcell',
10 'notebook/js/codecell',
10 'notebook/js/codecell',
11 'services/sessions/js/session',
11 'services/sessions/js/session',
12 'notebook/js/celltoolbar',
12 'notebook/js/celltoolbar',
13 'components/marked/lib/marked',
13 'components/marked/lib/marked',
14 'highlight',
14 'highlight',
15 'notebook/js/mathjaxutils',
15 'notebook/js/mathjaxutils',
16 'base/js/keyboard',
16 'base/js/keyboard',
17 'notebook/js/tooltip',
17 'notebook/js/tooltip',
18 'notebook/js/celltoolbarpresets/default',
18 'notebook/js/celltoolbarpresets/default',
19 'notebook/js/celltoolbarpresets/rawcell',
19 'notebook/js/celltoolbarpresets/rawcell',
20 'notebook/js/celltoolbarpresets/slideshow',
20 'notebook/js/celltoolbarpresets/slideshow',
21 ], function (
21 ], function (
22 IPython,
22 IPython,
23 $,
23 $,
24 utils,
24 utils,
25 dialog,
25 dialog,
26 textcell,
26 textcell,
27 codecell,
27 codecell,
28 session,
28 session,
29 celltoolbar,
29 celltoolbar,
30 marked,
30 marked,
31 hljs,
31 hljs,
32 mathjaxutils,
32 mathjaxutils,
33 keyboard,
33 keyboard,
34 tooltip,
34 tooltip,
35 default_celltoolbar,
35 default_celltoolbar,
36 rawcell_celltoolbar,
36 rawcell_celltoolbar,
37 slideshow_celltoolbar
37 slideshow_celltoolbar
38 ) {
38 ) {
39
39
40 var Notebook = function (selector, options) {
40 var Notebook = function (selector, options) {
41 // Constructor
41 // Constructor
42 //
42 //
43 // A notebook contains and manages cells.
43 // A notebook contains and manages cells.
44 //
44 //
45 // Parameters:
45 // Parameters:
46 // selector: string
46 // selector: string
47 // options: dictionary
47 // options: dictionary
48 // Dictionary of keyword arguments.
48 // Dictionary of keyword arguments.
49 // events: $(Events) instance
49 // events: $(Events) instance
50 // keyboard_manager: KeyboardManager instance
50 // keyboard_manager: KeyboardManager instance
51 // save_widget: SaveWidget instance
51 // save_widget: SaveWidget instance
52 // config: dictionary
52 // config: dictionary
53 // base_url : string
53 // base_url : string
54 // notebook_path : string
54 // notebook_path : string
55 // notebook_name : string
55 // notebook_name : string
56 this.config = options.config || {};
56 this.config = options.config || {};
57 this.base_url = options.base_url;
57 this.base_url = options.base_url;
58 this.notebook_path = options.notebook_path;
58 this.notebook_path = options.notebook_path;
59 this.notebook_name = options.notebook_name;
59 this.notebook_name = options.notebook_name;
60 this.events = options.events;
60 this.events = options.events;
61 this.keyboard_manager = options.keyboard_manager;
61 this.keyboard_manager = options.keyboard_manager;
62 this.save_widget = options.save_widget;
62 this.save_widget = options.save_widget;
63 this.tooltip = new tooltip.Tooltip(this.events);
63 this.tooltip = new tooltip.Tooltip(this.events);
64 this.ws_url = options.ws_url;
64 this.ws_url = options.ws_url;
65 // default_kernel_name is a temporary measure while we implement proper
65 // default_kernel_name is a temporary measure while we implement proper
66 // kernel selection and delayed start. Do not rely on it.
66 // kernel selection and delayed start. Do not rely on it.
67 this.default_kernel_name = 'python';
67 this.default_kernel_name = 'python';
68 // TODO: This code smells (and the other `= this` line a couple lines down)
68 // TODO: This code smells (and the other `= this` line a couple lines down)
69 // We need a better way to deal with circular instance references.
69 // We need a better way to deal with circular instance references.
70 this.keyboard_manager.notebook = this;
70 this.keyboard_manager.notebook = this;
71 this.save_widget.notebook = this;
71 this.save_widget.notebook = this;
72
72
73 mathjaxutils.init();
73 mathjaxutils.init();
74
74
75 if (marked) {
75 if (marked) {
76 marked.setOptions({
76 marked.setOptions({
77 gfm : true,
77 gfm : true,
78 tables: true,
78 tables: true,
79 langPrefix: "language-",
79 langPrefix: "language-",
80 highlight: function(code, lang) {
80 highlight: function(code, lang) {
81 if (!lang) {
81 if (!lang) {
82 // no language, no highlight
82 // no language, no highlight
83 return code;
83 return code;
84 }
84 }
85 var highlighted;
85 var highlighted;
86 try {
86 try {
87 highlighted = hljs.highlight(lang, code, false);
87 highlighted = hljs.highlight(lang, code, false);
88 } catch(err) {
88 } catch(err) {
89 highlighted = hljs.highlightAuto(code);
89 highlighted = hljs.highlightAuto(code);
90 }
90 }
91 return highlighted.value;
91 return highlighted.value;
92 }
92 }
93 });
93 });
94 }
94 }
95
95
96 this.element = $(selector);
96 this.element = $(selector);
97 this.element.scroll();
97 this.element.scroll();
98 this.element.data("notebook", this);
98 this.element.data("notebook", this);
99 this.next_prompt_number = 1;
99 this.next_prompt_number = 1;
100 this.session = null;
100 this.session = null;
101 this.kernel = null;
101 this.kernel = null;
102 this.clipboard = null;
102 this.clipboard = null;
103 this.undelete_backup = null;
103 this.undelete_backup = null;
104 this.undelete_index = null;
104 this.undelete_index = null;
105 this.undelete_below = false;
105 this.undelete_below = false;
106 this.paste_enabled = false;
106 this.paste_enabled = false;
107 // It is important to start out in command mode to match the intial mode
107 // It is important to start out in command mode to match the intial mode
108 // of the KeyboardManager.
108 // of the KeyboardManager.
109 this.mode = 'command';
109 this.mode = 'command';
110 this.set_dirty(false);
110 this.set_dirty(false);
111 this.metadata = {};
111 this.metadata = {};
112 this._checkpoint_after_save = false;
112 this._checkpoint_after_save = false;
113 this.last_checkpoint = null;
113 this.last_checkpoint = null;
114 this.checkpoints = [];
114 this.checkpoints = [];
115 this.autosave_interval = 0;
115 this.autosave_interval = 0;
116 this.autosave_timer = null;
116 this.autosave_timer = null;
117 // autosave *at most* every two minutes
117 // autosave *at most* every two minutes
118 this.minimum_autosave_interval = 120000;
118 this.minimum_autosave_interval = 120000;
119 // single worksheet for now
119 // single worksheet for now
120 this.worksheet_metadata = {};
120 this.worksheet_metadata = {};
121 this.notebook_name_blacklist_re = /[\/\\:]/;
121 this.notebook_name_blacklist_re = /[\/\\:]/;
122 this.nbformat = 3; // Increment this when changing the nbformat
122 this.nbformat = 3; // Increment this when changing the nbformat
123 this.nbformat_minor = 0; // Increment this when changing the nbformat
123 this.nbformat_minor = 0; // Increment this when changing the nbformat
124 this.codemirror_mode = 'ipython';
124 this.codemirror_mode = 'ipython';
125 this.create_elements();
125 this.create_elements();
126 this.bind_events();
126 this.bind_events();
127 this.save_notebook = function() { // don't allow save until notebook_loaded
127 this.save_notebook = function() { // don't allow save until notebook_loaded
128 this.save_notebook_error(null, null, "Load failed, save is disabled");
128 this.save_notebook_error(null, null, "Load failed, save is disabled");
129 };
129 };
130
130
131 // Trigger cell toolbar registration.
131 // Trigger cell toolbar registration.
132 default_celltoolbar.register(this);
132 default_celltoolbar.register(this);
133 rawcell_celltoolbar.register(this);
133 rawcell_celltoolbar.register(this);
134 slideshow_celltoolbar.register(this);
134 slideshow_celltoolbar.register(this);
135 };
135 };
136
136
137
137
138 /**
138 /**
139 * Create an HTML and CSS representation of the notebook.
139 * Create an HTML and CSS representation of the notebook.
140 *
140 *
141 * @method create_elements
141 * @method create_elements
142 */
142 */
143 Notebook.prototype.create_elements = function () {
143 Notebook.prototype.create_elements = function () {
144 var that = this;
144 var that = this;
145 this.element.attr('tabindex','-1');
145 this.element.attr('tabindex','-1');
146 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
146 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
147 // We add this end_space div to the end of the notebook div to:
147 // We add this end_space div to the end of the notebook div to:
148 // i) provide a margin between the last cell and the end of the notebook
148 // i) provide a margin between the last cell and the end of the notebook
149 // ii) to prevent the div from scrolling up when the last cell is being
149 // ii) to prevent the div from scrolling up when the last cell is being
150 // edited, but is too low on the page, which browsers will do automatically.
150 // edited, but is too low on the page, which browsers will do automatically.
151 var end_space = $('<div/>').addClass('end_space');
151 var end_space = $('<div/>').addClass('end_space');
152 end_space.dblclick(function (e) {
152 end_space.dblclick(function (e) {
153 var ncells = that.ncells();
153 var ncells = that.ncells();
154 that.insert_cell_below('code',ncells-1);
154 that.insert_cell_below('code',ncells-1);
155 });
155 });
156 this.element.append(this.container);
156 this.element.append(this.container);
157 this.container.append(end_space);
157 this.container.append(end_space);
158 };
158 };
159
159
160 /**
160 /**
161 * Bind JavaScript events: key presses and custom IPython events.
161 * Bind JavaScript events: key presses and custom IPython events.
162 *
162 *
163 * @method bind_events
163 * @method bind_events
164 */
164 */
165 Notebook.prototype.bind_events = function () {
165 Notebook.prototype.bind_events = function () {
166 var that = this;
166 var that = this;
167
167
168 this.events.on('set_next_input.Notebook', function (event, data) {
168 this.events.on('set_next_input.Notebook', function (event, data) {
169 var index = that.find_cell_index(data.cell);
169 var index = that.find_cell_index(data.cell);
170 var new_cell = that.insert_cell_below('code',index);
170 var new_cell = that.insert_cell_below('code',index);
171 new_cell.set_text(data.text);
171 new_cell.set_text(data.text);
172 that.dirty = true;
172 that.dirty = true;
173 });
173 });
174
174
175 this.events.on('set_dirty.Notebook', function (event, data) {
175 this.events.on('set_dirty.Notebook', function (event, data) {
176 that.dirty = data.value;
176 that.dirty = data.value;
177 });
177 });
178
178
179 this.events.on('trust_changed.Notebook', function (event, data) {
179 this.events.on('trust_changed.Notebook', function (event, data) {
180 that.trusted = data.value;
180 that.trusted = data.value;
181 });
181 });
182
182
183 this.events.on('select.Cell', function (event, data) {
183 this.events.on('select.Cell', function (event, data) {
184 var index = that.find_cell_index(data.cell);
184 var index = that.find_cell_index(data.cell);
185 that.select(index);
185 that.select(index);
186 });
186 });
187
187
188 this.events.on('edit_mode.Cell', function (event, data) {
188 this.events.on('edit_mode.Cell', function (event, data) {
189 that.handle_edit_mode(data.cell);
189 that.handle_edit_mode(data.cell);
190 });
190 });
191
191
192 this.events.on('command_mode.Cell', function (event, data) {
192 this.events.on('command_mode.Cell', function (event, data) {
193 that.handle_command_mode(data.cell);
193 that.handle_command_mode(data.cell);
194 });
194 });
195
195
196 this.events.on('status_autorestarting.Kernel', function () {
196 this.events.on('status_autorestarting.Kernel', function () {
197 dialog.modal({
197 dialog.modal({
198 notebook: that,
198 notebook: that,
199 keyboard_manager: that.keyboard_manager,
199 keyboard_manager: that.keyboard_manager,
200 title: "Kernel Restarting",
200 title: "Kernel Restarting",
201 body: "The kernel appears to have died. It will restart automatically.",
201 body: "The kernel appears to have died. It will restart automatically.",
202 buttons: {
202 buttons: {
203 OK : {
203 OK : {
204 class : "btn-primary"
204 class : "btn-primary"
205 }
205 }
206 }
206 }
207 });
207 });
208 });
208 });
209
209
210 this.events.on('spec_changed.Kernel', function(event, data) {
210 this.events.on('spec_changed.Kernel', function(event, data) {
211 that.set_kernelspec_metadata(data);
211 that.set_kernelspec_metadata(data);
212 if (data.codemirror_mode) {
212 if (data.codemirror_mode) {
213 that.set_codemirror_mode(data.codemirror_mode);
213 that.set_codemirror_mode(data.codemirror_mode);
214 }
214 }
215 });
215 });
216
216
217 var collapse_time = function (time) {
217 var collapse_time = function (time) {
218 var app_height = $('#ipython-main-app').height(); // content height
218 var app_height = $('#ipython-main-app').height(); // content height
219 var splitter_height = $('div#pager_splitter').outerHeight(true);
219 var splitter_height = $('div#pager_splitter').outerHeight(true);
220 var new_height = app_height - splitter_height;
220 var new_height = app_height - splitter_height;
221 that.element.animate({height : new_height + 'px'}, time);
221 that.element.animate({height : new_height + 'px'}, time);
222 };
222 };
223
223
224 this.element.bind('collapse_pager', function (event, extrap) {
224 this.element.bind('collapse_pager', function (event, extrap) {
225 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
225 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
226 collapse_time(time);
226 collapse_time(time);
227 });
227 });
228
228
229 var expand_time = function (time) {
229 var expand_time = function (time) {
230 var app_height = $('#ipython-main-app').height(); // content height
230 var app_height = $('#ipython-main-app').height(); // content height
231 var splitter_height = $('div#pager_splitter').outerHeight(true);
231 var splitter_height = $('div#pager_splitter').outerHeight(true);
232 var pager_height = $('div#pager').outerHeight(true);
232 var pager_height = $('div#pager').outerHeight(true);
233 var new_height = app_height - pager_height - splitter_height;
233 var new_height = app_height - pager_height - splitter_height;
234 that.element.animate({height : new_height + 'px'}, time);
234 that.element.animate({height : new_height + 'px'}, time);
235 };
235 };
236
236
237 this.element.bind('expand_pager', function (event, extrap) {
237 this.element.bind('expand_pager', function (event, extrap) {
238 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
238 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
239 expand_time(time);
239 expand_time(time);
240 });
240 });
241
241
242 // Firefox 22 broke $(window).on("beforeunload")
242 // Firefox 22 broke $(window).on("beforeunload")
243 // I'm not sure why or how.
243 // I'm not sure why or how.
244 window.onbeforeunload = function (e) {
244 window.onbeforeunload = function (e) {
245 // TODO: Make killing the kernel configurable.
245 // TODO: Make killing the kernel configurable.
246 var kill_kernel = false;
246 var kill_kernel = false;
247 if (kill_kernel) {
247 if (kill_kernel) {
248 that.session.kill_kernel();
248 that.session.kill_kernel();
249 }
249 }
250 // if we are autosaving, trigger an autosave on nav-away.
250 // if we are autosaving, trigger an autosave on nav-away.
251 // still warn, because if we don't the autosave may fail.
251 // still warn, because if we don't the autosave may fail.
252 if (that.dirty) {
252 if (that.dirty) {
253 if ( that.autosave_interval ) {
253 if ( that.autosave_interval ) {
254 // schedule autosave in a timeout
254 // schedule autosave in a timeout
255 // this gives you a chance to forcefully discard changes
255 // this gives you a chance to forcefully discard changes
256 // by reloading the page if you *really* want to.
256 // by reloading the page if you *really* want to.
257 // the timer doesn't start until you *dismiss* the dialog.
257 // the timer doesn't start until you *dismiss* the dialog.
258 setTimeout(function () {
258 setTimeout(function () {
259 if (that.dirty) {
259 if (that.dirty) {
260 that.save_notebook();
260 that.save_notebook();
261 }
261 }
262 }, 1000);
262 }, 1000);
263 return "Autosave in progress, latest changes may be lost.";
263 return "Autosave in progress, latest changes may be lost.";
264 } else {
264 } else {
265 return "Unsaved changes will be lost.";
265 return "Unsaved changes will be lost.";
266 }
266 }
267 }
267 }
268 // Null is the *only* return value that will make the browser not
268 // Null is the *only* return value that will make the browser not
269 // pop up the "don't leave" dialog.
269 // pop up the "don't leave" dialog.
270 return null;
270 return null;
271 };
271 };
272 };
272 };
273
273
274 /**
274 /**
275 * Set the dirty flag, and trigger the set_dirty.Notebook event
275 * Set the dirty flag, and trigger the set_dirty.Notebook event
276 *
276 *
277 * @method set_dirty
277 * @method set_dirty
278 */
278 */
279 Notebook.prototype.set_dirty = function (value) {
279 Notebook.prototype.set_dirty = function (value) {
280 if (value === undefined) {
280 if (value === undefined) {
281 value = true;
281 value = true;
282 }
282 }
283 if (this.dirty == value) {
283 if (this.dirty == value) {
284 return;
284 return;
285 }
285 }
286 this.events.trigger('set_dirty.Notebook', {value: value});
286 this.events.trigger('set_dirty.Notebook', {value: value});
287 };
287 };
288
288
289 /**
289 /**
290 * Scroll the top of the page to a given cell.
290 * Scroll the top of the page to a given cell.
291 *
291 *
292 * @method scroll_to_cell
292 * @method scroll_to_cell
293 * @param {Number} cell_number An index of the cell to view
293 * @param {Number} cell_number An index of the cell to view
294 * @param {Number} time Animation time in milliseconds
294 * @param {Number} time Animation time in milliseconds
295 * @return {Number} Pixel offset from the top of the container
295 * @return {Number} Pixel offset from the top of the container
296 */
296 */
297 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
297 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
298 var cells = this.get_cells();
298 var cells = this.get_cells();
299 time = time || 0;
299 time = time || 0;
300 cell_number = Math.min(cells.length-1,cell_number);
300 cell_number = Math.min(cells.length-1,cell_number);
301 cell_number = Math.max(0 ,cell_number);
301 cell_number = Math.max(0 ,cell_number);
302 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
302 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
303 this.element.animate({scrollTop:scroll_value}, time);
303 this.element.animate({scrollTop:scroll_value}, time);
304 return scroll_value;
304 return scroll_value;
305 };
305 };
306
306
307 /**
307 /**
308 * Scroll to the bottom of the page.
308 * Scroll to the bottom of the page.
309 *
309 *
310 * @method scroll_to_bottom
310 * @method scroll_to_bottom
311 */
311 */
312 Notebook.prototype.scroll_to_bottom = function () {
312 Notebook.prototype.scroll_to_bottom = function () {
313 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
313 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
314 };
314 };
315
315
316 /**
316 /**
317 * Scroll to the top of the page.
317 * Scroll to the top of the page.
318 *
318 *
319 * @method scroll_to_top
319 * @method scroll_to_top
320 */
320 */
321 Notebook.prototype.scroll_to_top = function () {
321 Notebook.prototype.scroll_to_top = function () {
322 this.element.animate({scrollTop:0}, 0);
322 this.element.animate({scrollTop:0}, 0);
323 };
323 };
324
324
325 // Edit Notebook metadata
325 // Edit Notebook metadata
326
326
327 Notebook.prototype.edit_metadata = function () {
327 Notebook.prototype.edit_metadata = function () {
328 var that = this;
328 var that = this;
329 dialog.edit_metadata({
329 dialog.edit_metadata({
330 md: this.metadata,
330 md: this.metadata,
331 callback: function (md) {
331 callback: function (md) {
332 that.metadata = md;
332 that.metadata = md;
333 },
333 },
334 name: 'Notebook',
334 name: 'Notebook',
335 notebook: this,
335 notebook: this,
336 keyboard_manager: this.keyboard_manager});
336 keyboard_manager: this.keyboard_manager});
337 };
337 };
338
338
339 Notebook.prototype.set_kernelspec_metadata = function(ks) {
339 Notebook.prototype.set_kernelspec_metadata = function(ks) {
340 var tostore = {};
340 var tostore = {};
341 $.map(ks, function(value, field) {
341 $.map(ks, function(value, field) {
342 if (field !== 'argv' && field !== 'env') {
342 if (field !== 'argv' && field !== 'env') {
343 tostore[field] = value;
343 tostore[field] = value;
344 }
344 }
345 });
345 });
346 this.metadata.kernelspec = tostore;
346 this.metadata.kernelspec = tostore;
347 }
347 }
348
348
349 // Cell indexing, retrieval, etc.
349 // Cell indexing, retrieval, etc.
350
350
351 /**
351 /**
352 * Get all cell elements in the notebook.
352 * Get all cell elements in the notebook.
353 *
353 *
354 * @method get_cell_elements
354 * @method get_cell_elements
355 * @return {jQuery} A selector of all cell elements
355 * @return {jQuery} A selector of all cell elements
356 */
356 */
357 Notebook.prototype.get_cell_elements = function () {
357 Notebook.prototype.get_cell_elements = function () {
358 return this.container.children("div.cell");
358 return this.container.children("div.cell");
359 };
359 };
360
360
361 /**
361 /**
362 * Get a particular cell element.
362 * Get a particular cell element.
363 *
363 *
364 * @method get_cell_element
364 * @method get_cell_element
365 * @param {Number} index An index of a cell to select
365 * @param {Number} index An index of a cell to select
366 * @return {jQuery} A selector of the given cell.
366 * @return {jQuery} A selector of the given cell.
367 */
367 */
368 Notebook.prototype.get_cell_element = function (index) {
368 Notebook.prototype.get_cell_element = function (index) {
369 var result = null;
369 var result = null;
370 var e = this.get_cell_elements().eq(index);
370 var e = this.get_cell_elements().eq(index);
371 if (e.length !== 0) {
371 if (e.length !== 0) {
372 result = e;
372 result = e;
373 }
373 }
374 return result;
374 return result;
375 };
375 };
376
376
377 /**
377 /**
378 * Try to get a particular cell by msg_id.
378 * Try to get a particular cell by msg_id.
379 *
379 *
380 * @method get_msg_cell
380 * @method get_msg_cell
381 * @param {String} msg_id A message UUID
381 * @param {String} msg_id A message UUID
382 * @return {Cell} Cell or null if no cell was found.
382 * @return {Cell} Cell or null if no cell was found.
383 */
383 */
384 Notebook.prototype.get_msg_cell = function (msg_id) {
384 Notebook.prototype.get_msg_cell = function (msg_id) {
385 return codecell.CodeCell.msg_cells[msg_id] || null;
385 return codecell.CodeCell.msg_cells[msg_id] || null;
386 };
386 };
387
387
388 /**
388 /**
389 * Count the cells in this notebook.
389 * Count the cells in this notebook.
390 *
390 *
391 * @method ncells
391 * @method ncells
392 * @return {Number} The number of cells in this notebook
392 * @return {Number} The number of cells in this notebook
393 */
393 */
394 Notebook.prototype.ncells = function () {
394 Notebook.prototype.ncells = function () {
395 return this.get_cell_elements().length;
395 return this.get_cell_elements().length;
396 };
396 };
397
397
398 /**
398 /**
399 * Get all Cell objects in this notebook.
399 * Get all Cell objects in this notebook.
400 *
400 *
401 * @method get_cells
401 * @method get_cells
402 * @return {Array} This notebook's Cell objects
402 * @return {Array} This notebook's Cell objects
403 */
403 */
404 // TODO: we are often calling cells as cells()[i], which we should optimize
404 // TODO: we are often calling cells as cells()[i], which we should optimize
405 // to cells(i) or a new method.
405 // to cells(i) or a new method.
406 Notebook.prototype.get_cells = function () {
406 Notebook.prototype.get_cells = function () {
407 return this.get_cell_elements().toArray().map(function (e) {
407 return this.get_cell_elements().toArray().map(function (e) {
408 return $(e).data("cell");
408 return $(e).data("cell");
409 });
409 });
410 };
410 };
411
411
412 /**
412 /**
413 * Get a Cell object from this notebook.
413 * Get a Cell object from this notebook.
414 *
414 *
415 * @method get_cell
415 * @method get_cell
416 * @param {Number} index An index of a cell to retrieve
416 * @param {Number} index An index of a cell to retrieve
417 * @return {Cell} A particular cell
417 * @return {Cell} A particular cell
418 */
418 */
419 Notebook.prototype.get_cell = function (index) {
419 Notebook.prototype.get_cell = function (index) {
420 var result = null;
420 var result = null;
421 var ce = this.get_cell_element(index);
421 var ce = this.get_cell_element(index);
422 if (ce !== null) {
422 if (ce !== null) {
423 result = ce.data('cell');
423 result = ce.data('cell');
424 }
424 }
425 return result;
425 return result;
426 };
426 };
427
427
428 /**
428 /**
429 * Get the cell below a given cell.
429 * Get the cell below a given cell.
430 *
430 *
431 * @method get_next_cell
431 * @method get_next_cell
432 * @param {Cell} cell The provided cell
432 * @param {Cell} cell The provided cell
433 * @return {Cell} The next cell
433 * @return {Cell} The next cell
434 */
434 */
435 Notebook.prototype.get_next_cell = function (cell) {
435 Notebook.prototype.get_next_cell = function (cell) {
436 var result = null;
436 var result = null;
437 var index = this.find_cell_index(cell);
437 var index = this.find_cell_index(cell);
438 if (this.is_valid_cell_index(index+1)) {
438 if (this.is_valid_cell_index(index+1)) {
439 result = this.get_cell(index+1);
439 result = this.get_cell(index+1);
440 }
440 }
441 return result;
441 return result;
442 };
442 };
443
443
444 /**
444 /**
445 * Get the cell above a given cell.
445 * Get the cell above a given cell.
446 *
446 *
447 * @method get_prev_cell
447 * @method get_prev_cell
448 * @param {Cell} cell The provided cell
448 * @param {Cell} cell The provided cell
449 * @return {Cell} The previous cell
449 * @return {Cell} The previous cell
450 */
450 */
451 Notebook.prototype.get_prev_cell = function (cell) {
451 Notebook.prototype.get_prev_cell = function (cell) {
452 // TODO: off-by-one
452 // TODO: off-by-one
453 // nb.get_prev_cell(nb.get_cell(1)) is null
453 // nb.get_prev_cell(nb.get_cell(1)) is null
454 var result = null;
454 var result = null;
455 var index = this.find_cell_index(cell);
455 var index = this.find_cell_index(cell);
456 if (index !== null && index > 1) {
456 if (index !== null && index > 1) {
457 result = this.get_cell(index-1);
457 result = this.get_cell(index-1);
458 }
458 }
459 return result;
459 return result;
460 };
460 };
461
461
462 /**
462 /**
463 * Get the numeric index of a given cell.
463 * Get the numeric index of a given cell.
464 *
464 *
465 * @method find_cell_index
465 * @method find_cell_index
466 * @param {Cell} cell The provided cell
466 * @param {Cell} cell The provided cell
467 * @return {Number} The cell's numeric index
467 * @return {Number} The cell's numeric index
468 */
468 */
469 Notebook.prototype.find_cell_index = function (cell) {
469 Notebook.prototype.find_cell_index = function (cell) {
470 var result = null;
470 var result = null;
471 this.get_cell_elements().filter(function (index) {
471 this.get_cell_elements().filter(function (index) {
472 if ($(this).data("cell") === cell) {
472 if ($(this).data("cell") === cell) {
473 result = index;
473 result = index;
474 }
474 }
475 });
475 });
476 return result;
476 return result;
477 };
477 };
478
478
479 /**
479 /**
480 * Get a given index , or the selected index if none is provided.
480 * Get a given index , or the selected index if none is provided.
481 *
481 *
482 * @method index_or_selected
482 * @method index_or_selected
483 * @param {Number} index A cell's index
483 * @param {Number} index A cell's index
484 * @return {Number} The given index, or selected index if none is provided.
484 * @return {Number} The given index, or selected index if none is provided.
485 */
485 */
486 Notebook.prototype.index_or_selected = function (index) {
486 Notebook.prototype.index_or_selected = function (index) {
487 var i;
487 var i;
488 if (index === undefined || index === null) {
488 if (index === undefined || index === null) {
489 i = this.get_selected_index();
489 i = this.get_selected_index();
490 if (i === null) {
490 if (i === null) {
491 i = 0;
491 i = 0;
492 }
492 }
493 } else {
493 } else {
494 i = index;
494 i = index;
495 }
495 }
496 return i;
496 return i;
497 };
497 };
498
498
499 /**
499 /**
500 * Get the currently selected cell.
500 * Get the currently selected cell.
501 * @method get_selected_cell
501 * @method get_selected_cell
502 * @return {Cell} The selected cell
502 * @return {Cell} The selected cell
503 */
503 */
504 Notebook.prototype.get_selected_cell = function () {
504 Notebook.prototype.get_selected_cell = function () {
505 var index = this.get_selected_index();
505 var index = this.get_selected_index();
506 return this.get_cell(index);
506 return this.get_cell(index);
507 };
507 };
508
508
509 /**
509 /**
510 * Check whether a cell index is valid.
510 * Check whether a cell index is valid.
511 *
511 *
512 * @method is_valid_cell_index
512 * @method is_valid_cell_index
513 * @param {Number} index A cell index
513 * @param {Number} index A cell index
514 * @return True if the index is valid, false otherwise
514 * @return True if the index is valid, false otherwise
515 */
515 */
516 Notebook.prototype.is_valid_cell_index = function (index) {
516 Notebook.prototype.is_valid_cell_index = function (index) {
517 if (index !== null && index >= 0 && index < this.ncells()) {
517 if (index !== null && index >= 0 && index < this.ncells()) {
518 return true;
518 return true;
519 } else {
519 } else {
520 return false;
520 return false;
521 }
521 }
522 };
522 };
523
523
524 /**
524 /**
525 * Get the index of the currently selected cell.
525 * Get the index of the currently selected cell.
526
526
527 * @method get_selected_index
527 * @method get_selected_index
528 * @return {Number} The selected cell's numeric index
528 * @return {Number} The selected cell's numeric index
529 */
529 */
530 Notebook.prototype.get_selected_index = function () {
530 Notebook.prototype.get_selected_index = function () {
531 var result = null;
531 var result = null;
532 this.get_cell_elements().filter(function (index) {
532 this.get_cell_elements().filter(function (index) {
533 if ($(this).data("cell").selected === true) {
533 if ($(this).data("cell").selected === true) {
534 result = index;
534 result = index;
535 }
535 }
536 });
536 });
537 return result;
537 return result;
538 };
538 };
539
539
540
540
541 // Cell selection.
541 // Cell selection.
542
542
543 /**
543 /**
544 * Programmatically select a cell.
544 * Programmatically select a cell.
545 *
545 *
546 * @method select
546 * @method select
547 * @param {Number} index A cell's index
547 * @param {Number} index A cell's index
548 * @return {Notebook} This notebook
548 * @return {Notebook} This notebook
549 */
549 */
550 Notebook.prototype.select = function (index) {
550 Notebook.prototype.select = function (index) {
551 if (this.is_valid_cell_index(index)) {
551 if (this.is_valid_cell_index(index)) {
552 var sindex = this.get_selected_index();
552 var sindex = this.get_selected_index();
553 if (sindex !== null && index !== sindex) {
553 if (sindex !== null && index !== sindex) {
554 // If we are about to select a different cell, make sure we are
554 // If we are about to select a different cell, make sure we are
555 // first in command mode.
555 // first in command mode.
556 if (this.mode !== 'command') {
556 if (this.mode !== 'command') {
557 this.command_mode();
557 this.command_mode();
558 }
558 }
559 this.get_cell(sindex).unselect();
559 this.get_cell(sindex).unselect();
560 }
560 }
561 var cell = this.get_cell(index);
561 var cell = this.get_cell(index);
562 cell.select();
562 cell.select();
563 if (cell.cell_type === 'heading') {
563 if (cell.cell_type === 'heading') {
564 this.events.trigger('selected_cell_type_changed.Notebook',
564 this.events.trigger('selected_cell_type_changed.Notebook',
565 {'cell_type':cell.cell_type,level:cell.level}
565 {'cell_type':cell.cell_type,level:cell.level}
566 );
566 );
567 } else {
567 } else {
568 this.events.trigger('selected_cell_type_changed.Notebook',
568 this.events.trigger('selected_cell_type_changed.Notebook',
569 {'cell_type':cell.cell_type}
569 {'cell_type':cell.cell_type}
570 );
570 );
571 }
571 }
572 }
572 }
573 return this;
573 return this;
574 };
574 };
575
575
576 /**
576 /**
577 * Programmatically select the next cell.
577 * Programmatically select the next cell.
578 *
578 *
579 * @method select_next
579 * @method select_next
580 * @return {Notebook} This notebook
580 * @return {Notebook} This notebook
581 */
581 */
582 Notebook.prototype.select_next = function () {
582 Notebook.prototype.select_next = function () {
583 var index = this.get_selected_index();
583 var index = this.get_selected_index();
584 this.select(index+1);
584 this.select(index+1);
585 return this;
585 return this;
586 };
586 };
587
587
588 /**
588 /**
589 * Programmatically select the previous cell.
589 * Programmatically select the previous cell.
590 *
590 *
591 * @method select_prev
591 * @method select_prev
592 * @return {Notebook} This notebook
592 * @return {Notebook} This notebook
593 */
593 */
594 Notebook.prototype.select_prev = function () {
594 Notebook.prototype.select_prev = function () {
595 var index = this.get_selected_index();
595 var index = this.get_selected_index();
596 this.select(index-1);
596 this.select(index-1);
597 return this;
597 return this;
598 };
598 };
599
599
600
600
601 // Edit/Command mode
601 // Edit/Command mode
602
602
603 /**
603 /**
604 * Gets the index of the cell that is in edit mode.
604 * Gets the index of the cell that is in edit mode.
605 *
605 *
606 * @method get_edit_index
606 * @method get_edit_index
607 *
607 *
608 * @return index {int}
608 * @return index {int}
609 **/
609 **/
610 Notebook.prototype.get_edit_index = function () {
610 Notebook.prototype.get_edit_index = function () {
611 var result = null;
611 var result = null;
612 this.get_cell_elements().filter(function (index) {
612 this.get_cell_elements().filter(function (index) {
613 if ($(this).data("cell").mode === 'edit') {
613 if ($(this).data("cell").mode === 'edit') {
614 result = index;
614 result = index;
615 }
615 }
616 });
616 });
617 return result;
617 return result;
618 };
618 };
619
619
620 /**
620 /**
621 * Handle when a a cell blurs and the notebook should enter command mode.
621 * Handle when a a cell blurs and the notebook should enter command mode.
622 *
622 *
623 * @method handle_command_mode
623 * @method handle_command_mode
624 * @param [cell] {Cell} Cell to enter command mode on.
624 * @param [cell] {Cell} Cell to enter command mode on.
625 **/
625 **/
626 Notebook.prototype.handle_command_mode = function (cell) {
626 Notebook.prototype.handle_command_mode = function (cell) {
627 if (this.mode !== 'command') {
627 if (this.mode !== 'command') {
628 cell.command_mode();
628 cell.command_mode();
629 this.mode = 'command';
629 this.mode = 'command';
630 this.events.trigger('command_mode.Notebook');
630 this.events.trigger('command_mode.Notebook');
631 this.keyboard_manager.command_mode();
631 this.keyboard_manager.command_mode();
632 }
632 }
633 };
633 };
634
634
635 /**
635 /**
636 * Make the notebook enter command mode.
636 * Make the notebook enter command mode.
637 *
637 *
638 * @method command_mode
638 * @method command_mode
639 **/
639 **/
640 Notebook.prototype.command_mode = function () {
640 Notebook.prototype.command_mode = function () {
641 var cell = this.get_cell(this.get_edit_index());
641 var cell = this.get_cell(this.get_edit_index());
642 if (cell && this.mode !== 'command') {
642 if (cell && this.mode !== 'command') {
643 // We don't call cell.command_mode, but rather call cell.focus_cell()
643 // We don't call cell.command_mode, but rather call cell.focus_cell()
644 // which will blur and CM editor and trigger the call to
644 // which will blur and CM editor and trigger the call to
645 // handle_command_mode.
645 // handle_command_mode.
646 cell.focus_cell();
646 cell.focus_cell();
647 }
647 }
648 };
648 };
649
649
650 /**
650 /**
651 * Handle when a cell fires it's edit_mode event.
651 * Handle when a cell fires it's edit_mode event.
652 *
652 *
653 * @method handle_edit_mode
653 * @method handle_edit_mode
654 * @param [cell] {Cell} Cell to enter edit mode on.
654 * @param [cell] {Cell} Cell to enter edit mode on.
655 **/
655 **/
656 Notebook.prototype.handle_edit_mode = function (cell) {
656 Notebook.prototype.handle_edit_mode = function (cell) {
657 if (cell && this.mode !== 'edit') {
657 if (cell && this.mode !== 'edit') {
658 cell.edit_mode();
658 cell.edit_mode();
659 this.mode = 'edit';
659 this.mode = 'edit';
660 this.events.trigger('edit_mode.Notebook');
660 this.events.trigger('edit_mode.Notebook');
661 this.keyboard_manager.edit_mode();
661 this.keyboard_manager.edit_mode();
662 }
662 }
663 };
663 };
664
664
665 /**
665 /**
666 * Make a cell enter edit mode.
666 * Make a cell enter edit mode.
667 *
667 *
668 * @method edit_mode
668 * @method edit_mode
669 **/
669 **/
670 Notebook.prototype.edit_mode = function () {
670 Notebook.prototype.edit_mode = function () {
671 var cell = this.get_selected_cell();
671 var cell = this.get_selected_cell();
672 if (cell && this.mode !== 'edit') {
672 if (cell && this.mode !== 'edit') {
673 cell.unrender();
673 cell.unrender();
674 cell.focus_editor();
674 cell.focus_editor();
675 }
675 }
676 };
676 };
677
677
678 /**
678 /**
679 * Focus the currently selected cell.
679 * Focus the currently selected cell.
680 *
680 *
681 * @method focus_cell
681 * @method focus_cell
682 **/
682 **/
683 Notebook.prototype.focus_cell = function () {
683 Notebook.prototype.focus_cell = function () {
684 var cell = this.get_selected_cell();
684 var cell = this.get_selected_cell();
685 if (cell === null) {return;} // No cell is selected
685 if (cell === null) {return;} // No cell is selected
686 cell.focus_cell();
686 cell.focus_cell();
687 };
687 };
688
688
689 // Cell movement
689 // Cell movement
690
690
691 /**
691 /**
692 * Move given (or selected) cell up and select it.
692 * Move given (or selected) cell up and select it.
693 *
693 *
694 * @method move_cell_up
694 * @method move_cell_up
695 * @param [index] {integer} cell index
695 * @param [index] {integer} cell index
696 * @return {Notebook} This notebook
696 * @return {Notebook} This notebook
697 **/
697 **/
698 Notebook.prototype.move_cell_up = function (index) {
698 Notebook.prototype.move_cell_up = function (index) {
699 var i = this.index_or_selected(index);
699 var i = this.index_or_selected(index);
700 if (this.is_valid_cell_index(i) && i > 0) {
700 if (this.is_valid_cell_index(i) && i > 0) {
701 var pivot = this.get_cell_element(i-1);
701 var pivot = this.get_cell_element(i-1);
702 var tomove = this.get_cell_element(i);
702 var tomove = this.get_cell_element(i);
703 if (pivot !== null && tomove !== null) {
703 if (pivot !== null && tomove !== null) {
704 tomove.detach();
704 tomove.detach();
705 pivot.before(tomove);
705 pivot.before(tomove);
706 this.select(i-1);
706 this.select(i-1);
707 var cell = this.get_selected_cell();
707 var cell = this.get_selected_cell();
708 cell.focus_cell();
708 cell.focus_cell();
709 }
709 }
710 this.set_dirty(true);
710 this.set_dirty(true);
711 }
711 }
712 return this;
712 return this;
713 };
713 };
714
714
715
715
716 /**
716 /**
717 * Move given (or selected) cell down and select it
717 * Move given (or selected) cell down and select it
718 *
718 *
719 * @method move_cell_down
719 * @method move_cell_down
720 * @param [index] {integer} cell index
720 * @param [index] {integer} cell index
721 * @return {Notebook} This notebook
721 * @return {Notebook} This notebook
722 **/
722 **/
723 Notebook.prototype.move_cell_down = function (index) {
723 Notebook.prototype.move_cell_down = function (index) {
724 var i = this.index_or_selected(index);
724 var i = this.index_or_selected(index);
725 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
725 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
726 var pivot = this.get_cell_element(i+1);
726 var pivot = this.get_cell_element(i+1);
727 var tomove = this.get_cell_element(i);
727 var tomove = this.get_cell_element(i);
728 if (pivot !== null && tomove !== null) {
728 if (pivot !== null && tomove !== null) {
729 tomove.detach();
729 tomove.detach();
730 pivot.after(tomove);
730 pivot.after(tomove);
731 this.select(i+1);
731 this.select(i+1);
732 var cell = this.get_selected_cell();
732 var cell = this.get_selected_cell();
733 cell.focus_cell();
733 cell.focus_cell();
734 }
734 }
735 }
735 }
736 this.set_dirty();
736 this.set_dirty();
737 return this;
737 return this;
738 };
738 };
739
739
740
740
741 // Insertion, deletion.
741 // Insertion, deletion.
742
742
743 /**
743 /**
744 * Delete a cell from the notebook.
744 * Delete a cell from the notebook.
745 *
745 *
746 * @method delete_cell
746 * @method delete_cell
747 * @param [index] A cell's numeric index
747 * @param [index] A cell's numeric index
748 * @return {Notebook} This notebook
748 * @return {Notebook} This notebook
749 */
749 */
750 Notebook.prototype.delete_cell = function (index) {
750 Notebook.prototype.delete_cell = function (index) {
751 var i = this.index_or_selected(index);
751 var i = this.index_or_selected(index);
752 var cell = this.get_selected_cell();
752 var cell = this.get_selected_cell();
753 this.undelete_backup = cell.toJSON();
753 this.undelete_backup = cell.toJSON();
754 $('#undelete_cell').removeClass('disabled');
754 $('#undelete_cell').removeClass('disabled');
755 if (this.is_valid_cell_index(i)) {
755 if (this.is_valid_cell_index(i)) {
756 var old_ncells = this.ncells();
756 var old_ncells = this.ncells();
757 var ce = this.get_cell_element(i);
757 var ce = this.get_cell_element(i);
758 ce.remove();
758 ce.remove();
759 if (i === 0) {
759 if (i === 0) {
760 // Always make sure we have at least one cell.
760 // Always make sure we have at least one cell.
761 if (old_ncells === 1) {
761 if (old_ncells === 1) {
762 this.insert_cell_below('code');
762 this.insert_cell_below('code');
763 }
763 }
764 this.select(0);
764 this.select(0);
765 this.undelete_index = 0;
765 this.undelete_index = 0;
766 this.undelete_below = false;
766 this.undelete_below = false;
767 } else if (i === old_ncells-1 && i !== 0) {
767 } else if (i === old_ncells-1 && i !== 0) {
768 this.select(i-1);
768 this.select(i-1);
769 this.undelete_index = i - 1;
769 this.undelete_index = i - 1;
770 this.undelete_below = true;
770 this.undelete_below = true;
771 } else {
771 } else {
772 this.select(i);
772 this.select(i);
773 this.undelete_index = i;
773 this.undelete_index = i;
774 this.undelete_below = false;
774 this.undelete_below = false;
775 }
775 }
776 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
776 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
777 this.set_dirty(true);
777 this.set_dirty(true);
778 }
778 }
779 return this;
779 return this;
780 };
780 };
781
781
782 /**
782 /**
783 * Restore the most recently deleted cell.
783 * Restore the most recently deleted cell.
784 *
784 *
785 * @method undelete
785 * @method undelete
786 */
786 */
787 Notebook.prototype.undelete_cell = function() {
787 Notebook.prototype.undelete_cell = function() {
788 if (this.undelete_backup !== null && this.undelete_index !== null) {
788 if (this.undelete_backup !== null && this.undelete_index !== null) {
789 var current_index = this.get_selected_index();
789 var current_index = this.get_selected_index();
790 if (this.undelete_index < current_index) {
790 if (this.undelete_index < current_index) {
791 current_index = current_index + 1;
791 current_index = current_index + 1;
792 }
792 }
793 if (this.undelete_index >= this.ncells()) {
793 if (this.undelete_index >= this.ncells()) {
794 this.select(this.ncells() - 1);
794 this.select(this.ncells() - 1);
795 }
795 }
796 else {
796 else {
797 this.select(this.undelete_index);
797 this.select(this.undelete_index);
798 }
798 }
799 var cell_data = this.undelete_backup;
799 var cell_data = this.undelete_backup;
800 var new_cell = null;
800 var new_cell = null;
801 if (this.undelete_below) {
801 if (this.undelete_below) {
802 new_cell = this.insert_cell_below(cell_data.cell_type);
802 new_cell = this.insert_cell_below(cell_data.cell_type);
803 } else {
803 } else {
804 new_cell = this.insert_cell_above(cell_data.cell_type);
804 new_cell = this.insert_cell_above(cell_data.cell_type);
805 }
805 }
806 new_cell.fromJSON(cell_data);
806 new_cell.fromJSON(cell_data);
807 if (this.undelete_below) {
807 if (this.undelete_below) {
808 this.select(current_index+1);
808 this.select(current_index+1);
809 } else {
809 } else {
810 this.select(current_index);
810 this.select(current_index);
811 }
811 }
812 this.undelete_backup = null;
812 this.undelete_backup = null;
813 this.undelete_index = null;
813 this.undelete_index = null;
814 }
814 }
815 $('#undelete_cell').addClass('disabled');
815 $('#undelete_cell').addClass('disabled');
816 };
816 };
817
817
818 /**
818 /**
819 * Insert a cell so that after insertion the cell is at given index.
819 * Insert a cell so that after insertion the cell is at given index.
820 *
820 *
821 * If cell type is not provided, it will default to the type of the
821 * If cell type is not provided, it will default to the type of the
822 * currently active cell.
822 * currently active cell.
823 *
823 *
824 * Similar to insert_above, but index parameter is mandatory
824 * Similar to insert_above, but index parameter is mandatory
825 *
825 *
826 * Index will be brought back into the accessible range [0,n]
826 * Index will be brought back into the accessible range [0,n]
827 *
827 *
828 * @method insert_cell_at_index
828 * @method insert_cell_at_index
829 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
829 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
830 * @param [index] {int} a valid index where to insert cell
830 * @param [index] {int} a valid index where to insert cell
831 *
831 *
832 * @return cell {cell|null} created cell or null
832 * @return cell {cell|null} created cell or null
833 **/
833 **/
834 Notebook.prototype.insert_cell_at_index = function(type, index){
834 Notebook.prototype.insert_cell_at_index = function(type, index){
835
835
836 var ncells = this.ncells();
836 var ncells = this.ncells();
837 index = Math.min(index,ncells);
837 index = Math.min(index,ncells);
838 index = Math.max(index,0);
838 index = Math.max(index,0);
839 var cell = null;
839 var cell = null;
840 type = type || this.get_selected_cell().cell_type;
840 type = type || this.get_selected_cell().cell_type;
841
841
842 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
842 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
843 var cell_options = {
843 var cell_options = {
844 events: this.events,
844 events: this.events,
845 config: this.config,
845 config: this.config,
846 keyboard_manager: this.keyboard_manager,
846 keyboard_manager: this.keyboard_manager,
847 notebook: this,
847 notebook: this,
848 tooltip: this.tooltip,
848 tooltip: this.tooltip,
849 };
849 };
850 if (type === 'code') {
850 if (type === 'code') {
851 cell = new codecell.CodeCell(this.kernel, cell_options);
851 cell = new codecell.CodeCell(this.kernel, cell_options);
852 cell.set_input_prompt();
852 cell.set_input_prompt();
853 } else if (type === 'markdown') {
853 } else if (type === 'markdown') {
854 cell = new textcell.MarkdownCell(cell_options);
854 cell = new textcell.MarkdownCell(cell_options);
855 } else if (type === 'raw') {
855 } else if (type === 'raw') {
856 cell = new textcell.RawCell(cell_options);
856 cell = new textcell.RawCell(cell_options);
857 } else if (type === 'heading') {
857 } else if (type === 'heading') {
858 cell = new textcell.HeadingCell(cell_options);
858 cell = new textcell.HeadingCell(cell_options);
859 }
859 }
860
860
861 if(this._insert_element_at_index(cell.element,index)) {
861 if(this._insert_element_at_index(cell.element,index)) {
862 cell.render();
862 cell.render();
863 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
863 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
864 cell.refresh();
864 cell.refresh();
865 // We used to select the cell after we refresh it, but there
865 // We used to select the cell after we refresh it, but there
866 // are now cases were this method is called where select is
866 // are now cases were this method is called where select is
867 // not appropriate. The selection logic should be handled by the
867 // not appropriate. The selection logic should be handled by the
868 // caller of the the top level insert_cell methods.
868 // caller of the the top level insert_cell methods.
869 this.set_dirty(true);
869 this.set_dirty(true);
870 }
870 }
871 }
871 }
872 return cell;
872 return cell;
873
873
874 };
874 };
875
875
876 /**
876 /**
877 * Insert an element at given cell index.
877 * Insert an element at given cell index.
878 *
878 *
879 * @method _insert_element_at_index
879 * @method _insert_element_at_index
880 * @param element {dom element} a cell element
880 * @param element {dom element} a cell element
881 * @param [index] {int} a valid index where to inser cell
881 * @param [index] {int} a valid index where to inser cell
882 * @private
882 * @private
883 *
883 *
884 * return true if everything whent fine.
884 * return true if everything whent fine.
885 **/
885 **/
886 Notebook.prototype._insert_element_at_index = function(element, index){
886 Notebook.prototype._insert_element_at_index = function(element, index){
887 if (element === undefined){
887 if (element === undefined){
888 return false;
888 return false;
889 }
889 }
890
890
891 var ncells = this.ncells();
891 var ncells = this.ncells();
892
892
893 if (ncells === 0) {
893 if (ncells === 0) {
894 // special case append if empty
894 // special case append if empty
895 this.element.find('div.end_space').before(element);
895 this.element.find('div.end_space').before(element);
896 } else if ( ncells === index ) {
896 } else if ( ncells === index ) {
897 // special case append it the end, but not empty
897 // special case append it the end, but not empty
898 this.get_cell_element(index-1).after(element);
898 this.get_cell_element(index-1).after(element);
899 } else if (this.is_valid_cell_index(index)) {
899 } else if (this.is_valid_cell_index(index)) {
900 // otherwise always somewhere to append to
900 // otherwise always somewhere to append to
901 this.get_cell_element(index).before(element);
901 this.get_cell_element(index).before(element);
902 } else {
902 } else {
903 return false;
903 return false;
904 }
904 }
905
905
906 if (this.undelete_index !== null && index <= this.undelete_index) {
906 if (this.undelete_index !== null && index <= this.undelete_index) {
907 this.undelete_index = this.undelete_index + 1;
907 this.undelete_index = this.undelete_index + 1;
908 this.set_dirty(true);
908 this.set_dirty(true);
909 }
909 }
910 return true;
910 return true;
911 };
911 };
912
912
913 /**
913 /**
914 * Insert a cell of given type above given index, or at top
914 * Insert a cell of given type above given index, or at top
915 * of notebook if index smaller than 0.
915 * of notebook if index smaller than 0.
916 *
916 *
917 * default index value is the one of currently selected cell
917 * default index value is the one of currently selected cell
918 *
918 *
919 * @method insert_cell_above
919 * @method insert_cell_above
920 * @param [type] {string} cell type
920 * @param [type] {string} cell type
921 * @param [index] {integer}
921 * @param [index] {integer}
922 *
922 *
923 * @return handle to created cell or null
923 * @return handle to created cell or null
924 **/
924 **/
925 Notebook.prototype.insert_cell_above = function (type, index) {
925 Notebook.prototype.insert_cell_above = function (type, index) {
926 index = this.index_or_selected(index);
926 index = this.index_or_selected(index);
927 return this.insert_cell_at_index(type, index);
927 return this.insert_cell_at_index(type, index);
928 };
928 };
929
929
930 /**
930 /**
931 * Insert a cell of given type below given index, or at bottom
931 * Insert a cell of given type below given index, or at bottom
932 * of notebook if index greater than number of cells
932 * of notebook if index greater than number of cells
933 *
933 *
934 * default index value is the one of currently selected cell
934 * default index value is the one of currently selected cell
935 *
935 *
936 * @method insert_cell_below
936 * @method insert_cell_below
937 * @param [type] {string} cell type
937 * @param [type] {string} cell type
938 * @param [index] {integer}
938 * @param [index] {integer}
939 *
939 *
940 * @return handle to created cell or null
940 * @return handle to created cell or null
941 *
941 *
942 **/
942 **/
943 Notebook.prototype.insert_cell_below = function (type, index) {
943 Notebook.prototype.insert_cell_below = function (type, index) {
944 index = this.index_or_selected(index);
944 index = this.index_or_selected(index);
945 return this.insert_cell_at_index(type, index+1);
945 return this.insert_cell_at_index(type, index+1);
946 };
946 };
947
947
948
948
949 /**
949 /**
950 * Insert cell at end of notebook
950 * Insert cell at end of notebook
951 *
951 *
952 * @method insert_cell_at_bottom
952 * @method insert_cell_at_bottom
953 * @param {String} type cell type
953 * @param {String} type cell type
954 *
954 *
955 * @return the added cell; or null
955 * @return the added cell; or null
956 **/
956 **/
957 Notebook.prototype.insert_cell_at_bottom = function (type){
957 Notebook.prototype.insert_cell_at_bottom = function (type){
958 var len = this.ncells();
958 var len = this.ncells();
959 return this.insert_cell_below(type,len-1);
959 return this.insert_cell_below(type,len-1);
960 };
960 };
961
961
962 /**
962 /**
963 * Turn a cell into a code cell.
963 * Turn a cell into a code cell.
964 *
964 *
965 * @method to_code
965 * @method to_code
966 * @param {Number} [index] A cell's index
966 * @param {Number} [index] A cell's index
967 */
967 */
968 Notebook.prototype.to_code = function (index) {
968 Notebook.prototype.to_code = function (index) {
969 var i = this.index_or_selected(index);
969 var i = this.index_or_selected(index);
970 if (this.is_valid_cell_index(i)) {
970 if (this.is_valid_cell_index(i)) {
971 var source_element = this.get_cell_element(i);
971 var source_element = this.get_cell_element(i);
972 var source_cell = source_element.data("cell");
972 var source_cell = source_element.data("cell");
973 if (!(source_cell instanceof codecell.CodeCell)) {
973 if (!(source_cell instanceof codecell.CodeCell)) {
974 var target_cell = this.insert_cell_below('code',i);
974 var target_cell = this.insert_cell_below('code',i);
975 var text = source_cell.get_text();
975 var text = source_cell.get_text();
976 if (text === source_cell.placeholder) {
976 if (text === source_cell.placeholder) {
977 text = '';
977 text = '';
978 }
978 }
979 target_cell.set_text(text);
979 target_cell.set_text(text);
980 // make this value the starting point, so that we can only undo
980 // make this value the starting point, so that we can only undo
981 // to this state, instead of a blank cell
981 // to this state, instead of a blank cell
982 target_cell.code_mirror.clearHistory();
982 target_cell.code_mirror.clearHistory();
983 source_element.remove();
983 source_element.remove();
984 this.select(i);
984 this.select(i);
985 var cursor = source_cell.code_mirror.getCursor();
985 var cursor = source_cell.code_mirror.getCursor();
986 target_cell.code_mirror.setCursor(cursor);
986 target_cell.code_mirror.setCursor(cursor);
987 this.set_dirty(true);
987 this.set_dirty(true);
988 }
988 }
989 }
989 }
990 };
990 };
991
991
992 /**
992 /**
993 * Turn a cell into a Markdown cell.
993 * Turn a cell into a Markdown cell.
994 *
994 *
995 * @method to_markdown
995 * @method to_markdown
996 * @param {Number} [index] A cell's index
996 * @param {Number} [index] A cell's index
997 */
997 */
998 Notebook.prototype.to_markdown = function (index) {
998 Notebook.prototype.to_markdown = function (index) {
999 var i = this.index_or_selected(index);
999 var i = this.index_or_selected(index);
1000 if (this.is_valid_cell_index(i)) {
1000 if (this.is_valid_cell_index(i)) {
1001 var source_element = this.get_cell_element(i);
1001 var source_element = this.get_cell_element(i);
1002 var source_cell = source_element.data("cell");
1002 var source_cell = source_element.data("cell");
1003 if (!(source_cell instanceof textcell.MarkdownCell)) {
1003 if (!(source_cell instanceof textcell.MarkdownCell)) {
1004 var target_cell = this.insert_cell_below('markdown',i);
1004 var target_cell = this.insert_cell_below('markdown',i);
1005 var text = source_cell.get_text();
1005 var text = source_cell.get_text();
1006 if (text === source_cell.placeholder) {
1006 if (text === source_cell.placeholder) {
1007 text = '';
1007 text = '';
1008 }
1008 }
1009 // We must show the editor before setting its contents
1009 // We must show the editor before setting its contents
1010 target_cell.unrender();
1010 target_cell.unrender();
1011 target_cell.set_text(text);
1011 target_cell.set_text(text);
1012 // make this value the starting point, so that we can only undo
1012 // make this value the starting point, so that we can only undo
1013 // to this state, instead of a blank cell
1013 // to this state, instead of a blank cell
1014 target_cell.code_mirror.clearHistory();
1014 target_cell.code_mirror.clearHistory();
1015 source_element.remove();
1015 source_element.remove();
1016 this.select(i);
1016 this.select(i);
1017 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1017 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1018 target_cell.render();
1018 target_cell.render();
1019 }
1019 }
1020 var cursor = source_cell.code_mirror.getCursor();
1020 var cursor = source_cell.code_mirror.getCursor();
1021 target_cell.code_mirror.setCursor(cursor);
1021 target_cell.code_mirror.setCursor(cursor);
1022 this.set_dirty(true);
1022 this.set_dirty(true);
1023 }
1023 }
1024 }
1024 }
1025 };
1025 };
1026
1026
1027 /**
1027 /**
1028 * Turn a cell into a raw text cell.
1028 * Turn a cell into a raw text cell.
1029 *
1029 *
1030 * @method to_raw
1030 * @method to_raw
1031 * @param {Number} [index] A cell's index
1031 * @param {Number} [index] A cell's index
1032 */
1032 */
1033 Notebook.prototype.to_raw = function (index) {
1033 Notebook.prototype.to_raw = function (index) {
1034 var i = this.index_or_selected(index);
1034 var i = this.index_or_selected(index);
1035 if (this.is_valid_cell_index(i)) {
1035 if (this.is_valid_cell_index(i)) {
1036 var source_element = this.get_cell_element(i);
1036 var source_element = this.get_cell_element(i);
1037 var source_cell = source_element.data("cell");
1037 var source_cell = source_element.data("cell");
1038 var target_cell = null;
1038 var target_cell = null;
1039 if (!(source_cell instanceof textcell.RawCell)) {
1039 if (!(source_cell instanceof textcell.RawCell)) {
1040 target_cell = this.insert_cell_below('raw',i);
1040 target_cell = this.insert_cell_below('raw',i);
1041 var text = source_cell.get_text();
1041 var text = source_cell.get_text();
1042 if (text === source_cell.placeholder) {
1042 if (text === source_cell.placeholder) {
1043 text = '';
1043 text = '';
1044 }
1044 }
1045 // We must show the editor before setting its contents
1045 // We must show the editor before setting its contents
1046 target_cell.unrender();
1046 target_cell.unrender();
1047 target_cell.set_text(text);
1047 target_cell.set_text(text);
1048 // make this value the starting point, so that we can only undo
1048 // make this value the starting point, so that we can only undo
1049 // to this state, instead of a blank cell
1049 // to this state, instead of a blank cell
1050 target_cell.code_mirror.clearHistory();
1050 target_cell.code_mirror.clearHistory();
1051 source_element.remove();
1051 source_element.remove();
1052 this.select(i);
1052 this.select(i);
1053 var cursor = source_cell.code_mirror.getCursor();
1053 var cursor = source_cell.code_mirror.getCursor();
1054 target_cell.code_mirror.setCursor(cursor);
1054 target_cell.code_mirror.setCursor(cursor);
1055 this.set_dirty(true);
1055 this.set_dirty(true);
1056 }
1056 }
1057 }
1057 }
1058 };
1058 };
1059
1059
1060 /**
1060 /**
1061 * Turn a cell into a heading cell.
1061 * Turn a cell into a heading cell.
1062 *
1062 *
1063 * @method to_heading
1063 * @method to_heading
1064 * @param {Number} [index] A cell's index
1064 * @param {Number} [index] A cell's index
1065 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1065 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1066 */
1066 */
1067 Notebook.prototype.to_heading = function (index, level) {
1067 Notebook.prototype.to_heading = function (index, level) {
1068 level = level || 1;
1068 level = level || 1;
1069 var i = this.index_or_selected(index);
1069 var i = this.index_or_selected(index);
1070 if (this.is_valid_cell_index(i)) {
1070 if (this.is_valid_cell_index(i)) {
1071 var source_element = this.get_cell_element(i);
1071 var source_element = this.get_cell_element(i);
1072 var source_cell = source_element.data("cell");
1072 var source_cell = source_element.data("cell");
1073 var target_cell = null;
1073 var target_cell = null;
1074 if (source_cell instanceof textcell.HeadingCell) {
1074 if (source_cell instanceof textcell.HeadingCell) {
1075 source_cell.set_level(level);
1075 source_cell.set_level(level);
1076 } else {
1076 } else {
1077 target_cell = this.insert_cell_below('heading',i);
1077 target_cell = this.insert_cell_below('heading',i);
1078 var text = source_cell.get_text();
1078 var text = source_cell.get_text();
1079 if (text === source_cell.placeholder) {
1079 if (text === source_cell.placeholder) {
1080 text = '';
1080 text = '';
1081 }
1081 }
1082 // We must show the editor before setting its contents
1082 // We must show the editor before setting its contents
1083 target_cell.set_level(level);
1083 target_cell.set_level(level);
1084 target_cell.unrender();
1084 target_cell.unrender();
1085 target_cell.set_text(text);
1085 target_cell.set_text(text);
1086 // make this value the starting point, so that we can only undo
1086 // make this value the starting point, so that we can only undo
1087 // to this state, instead of a blank cell
1087 // to this state, instead of a blank cell
1088 target_cell.code_mirror.clearHistory();
1088 target_cell.code_mirror.clearHistory();
1089 source_element.remove();
1089 source_element.remove();
1090 this.select(i);
1090 this.select(i);
1091 var cursor = source_cell.code_mirror.getCursor();
1091 var cursor = source_cell.code_mirror.getCursor();
1092 target_cell.code_mirror.setCursor(cursor);
1092 target_cell.code_mirror.setCursor(cursor);
1093 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1093 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1094 target_cell.render();
1094 target_cell.render();
1095 }
1095 }
1096 }
1096 }
1097 this.set_dirty(true);
1097 this.set_dirty(true);
1098 this.events.trigger('selected_cell_type_changed.Notebook',
1098 this.events.trigger('selected_cell_type_changed.Notebook',
1099 {'cell_type':'heading',level:level}
1099 {'cell_type':'heading',level:level}
1100 );
1100 );
1101 }
1101 }
1102 };
1102 };
1103
1103
1104
1104
1105 // Cut/Copy/Paste
1105 // Cut/Copy/Paste
1106
1106
1107 /**
1107 /**
1108 * Enable UI elements for pasting cells.
1108 * Enable UI elements for pasting cells.
1109 *
1109 *
1110 * @method enable_paste
1110 * @method enable_paste
1111 */
1111 */
1112 Notebook.prototype.enable_paste = function () {
1112 Notebook.prototype.enable_paste = function () {
1113 var that = this;
1113 var that = this;
1114 if (!this.paste_enabled) {
1114 if (!this.paste_enabled) {
1115 $('#paste_cell_replace').removeClass('disabled')
1115 $('#paste_cell_replace').removeClass('disabled')
1116 .on('click', function () {that.paste_cell_replace();});
1116 .on('click', function () {that.paste_cell_replace();});
1117 $('#paste_cell_above').removeClass('disabled')
1117 $('#paste_cell_above').removeClass('disabled')
1118 .on('click', function () {that.paste_cell_above();});
1118 .on('click', function () {that.paste_cell_above();});
1119 $('#paste_cell_below').removeClass('disabled')
1119 $('#paste_cell_below').removeClass('disabled')
1120 .on('click', function () {that.paste_cell_below();});
1120 .on('click', function () {that.paste_cell_below();});
1121 this.paste_enabled = true;
1121 this.paste_enabled = true;
1122 }
1122 }
1123 };
1123 };
1124
1124
1125 /**
1125 /**
1126 * Disable UI elements for pasting cells.
1126 * Disable UI elements for pasting cells.
1127 *
1127 *
1128 * @method disable_paste
1128 * @method disable_paste
1129 */
1129 */
1130 Notebook.prototype.disable_paste = function () {
1130 Notebook.prototype.disable_paste = function () {
1131 if (this.paste_enabled) {
1131 if (this.paste_enabled) {
1132 $('#paste_cell_replace').addClass('disabled').off('click');
1132 $('#paste_cell_replace').addClass('disabled').off('click');
1133 $('#paste_cell_above').addClass('disabled').off('click');
1133 $('#paste_cell_above').addClass('disabled').off('click');
1134 $('#paste_cell_below').addClass('disabled').off('click');
1134 $('#paste_cell_below').addClass('disabled').off('click');
1135 this.paste_enabled = false;
1135 this.paste_enabled = false;
1136 }
1136 }
1137 };
1137 };
1138
1138
1139 /**
1139 /**
1140 * Cut a cell.
1140 * Cut a cell.
1141 *
1141 *
1142 * @method cut_cell
1142 * @method cut_cell
1143 */
1143 */
1144 Notebook.prototype.cut_cell = function () {
1144 Notebook.prototype.cut_cell = function () {
1145 this.copy_cell();
1145 this.copy_cell();
1146 this.delete_cell();
1146 this.delete_cell();
1147 };
1147 };
1148
1148
1149 /**
1149 /**
1150 * Copy a cell.
1150 * Copy a cell.
1151 *
1151 *
1152 * @method copy_cell
1152 * @method copy_cell
1153 */
1153 */
1154 Notebook.prototype.copy_cell = function () {
1154 Notebook.prototype.copy_cell = function () {
1155 var cell = this.get_selected_cell();
1155 var cell = this.get_selected_cell();
1156 this.clipboard = cell.toJSON();
1156 this.clipboard = cell.toJSON();
1157 this.enable_paste();
1157 this.enable_paste();
1158 };
1158 };
1159
1159
1160 /**
1160 /**
1161 * Replace the selected cell with a cell in the clipboard.
1161 * Replace the selected cell with a cell in the clipboard.
1162 *
1162 *
1163 * @method paste_cell_replace
1163 * @method paste_cell_replace
1164 */
1164 */
1165 Notebook.prototype.paste_cell_replace = function () {
1165 Notebook.prototype.paste_cell_replace = function () {
1166 if (this.clipboard !== null && this.paste_enabled) {
1166 if (this.clipboard !== null && this.paste_enabled) {
1167 var cell_data = this.clipboard;
1167 var cell_data = this.clipboard;
1168 var new_cell = this.insert_cell_above(cell_data.cell_type);
1168 var new_cell = this.insert_cell_above(cell_data.cell_type);
1169 new_cell.fromJSON(cell_data);
1169 new_cell.fromJSON(cell_data);
1170 var old_cell = this.get_next_cell(new_cell);
1170 var old_cell = this.get_next_cell(new_cell);
1171 this.delete_cell(this.find_cell_index(old_cell));
1171 this.delete_cell(this.find_cell_index(old_cell));
1172 this.select(this.find_cell_index(new_cell));
1172 this.select(this.find_cell_index(new_cell));
1173 }
1173 }
1174 };
1174 };
1175
1175
1176 /**
1176 /**
1177 * Paste a cell from the clipboard above the selected cell.
1177 * Paste a cell from the clipboard above the selected cell.
1178 *
1178 *
1179 * @method paste_cell_above
1179 * @method paste_cell_above
1180 */
1180 */
1181 Notebook.prototype.paste_cell_above = function () {
1181 Notebook.prototype.paste_cell_above = function () {
1182 if (this.clipboard !== null && this.paste_enabled) {
1182 if (this.clipboard !== null && this.paste_enabled) {
1183 var cell_data = this.clipboard;
1183 var cell_data = this.clipboard;
1184 var new_cell = this.insert_cell_above(cell_data.cell_type);
1184 var new_cell = this.insert_cell_above(cell_data.cell_type);
1185 new_cell.fromJSON(cell_data);
1185 new_cell.fromJSON(cell_data);
1186 new_cell.focus_cell();
1186 new_cell.focus_cell();
1187 }
1187 }
1188 };
1188 };
1189
1189
1190 /**
1190 /**
1191 * Paste a cell from the clipboard below the selected cell.
1191 * Paste a cell from the clipboard below the selected cell.
1192 *
1192 *
1193 * @method paste_cell_below
1193 * @method paste_cell_below
1194 */
1194 */
1195 Notebook.prototype.paste_cell_below = function () {
1195 Notebook.prototype.paste_cell_below = function () {
1196 if (this.clipboard !== null && this.paste_enabled) {
1196 if (this.clipboard !== null && this.paste_enabled) {
1197 var cell_data = this.clipboard;
1197 var cell_data = this.clipboard;
1198 var new_cell = this.insert_cell_below(cell_data.cell_type);
1198 var new_cell = this.insert_cell_below(cell_data.cell_type);
1199 new_cell.fromJSON(cell_data);
1199 new_cell.fromJSON(cell_data);
1200 new_cell.focus_cell();
1200 new_cell.focus_cell();
1201 }
1201 }
1202 };
1202 };
1203
1203
1204 // Split/merge
1204 // Split/merge
1205
1205
1206 /**
1206 /**
1207 * Split the selected cell into two, at the cursor.
1207 * Split the selected cell into two, at the cursor.
1208 *
1208 *
1209 * @method split_cell
1209 * @method split_cell
1210 */
1210 */
1211 Notebook.prototype.split_cell = function () {
1211 Notebook.prototype.split_cell = function () {
1212 var mdc = textcell.MarkdownCell;
1212 var mdc = textcell.MarkdownCell;
1213 var rc = textcell.RawCell;
1213 var rc = textcell.RawCell;
1214 var cell = this.get_selected_cell();
1214 var cell = this.get_selected_cell();
1215 if (cell.is_splittable()) {
1215 if (cell.is_splittable()) {
1216 var texta = cell.get_pre_cursor();
1216 var texta = cell.get_pre_cursor();
1217 var textb = cell.get_post_cursor();
1217 var textb = cell.get_post_cursor();
1218 cell.set_text(textb);
1218 cell.set_text(textb);
1219 var new_cell = this.insert_cell_above(cell.cell_type);
1219 var new_cell = this.insert_cell_above(cell.cell_type);
1220 // Unrender the new cell so we can call set_text.
1220 // Unrender the new cell so we can call set_text.
1221 new_cell.unrender();
1221 new_cell.unrender();
1222 new_cell.set_text(texta);
1222 new_cell.set_text(texta);
1223 }
1223 }
1224 };
1224 };
1225
1225
1226 /**
1226 /**
1227 * Combine the selected cell into the cell above it.
1227 * Combine the selected cell into the cell above it.
1228 *
1228 *
1229 * @method merge_cell_above
1229 * @method merge_cell_above
1230 */
1230 */
1231 Notebook.prototype.merge_cell_above = function () {
1231 Notebook.prototype.merge_cell_above = function () {
1232 var mdc = textcell.MarkdownCell;
1232 var mdc = textcell.MarkdownCell;
1233 var rc = textcell.RawCell;
1233 var rc = textcell.RawCell;
1234 var index = this.get_selected_index();
1234 var index = this.get_selected_index();
1235 var cell = this.get_cell(index);
1235 var cell = this.get_cell(index);
1236 var render = cell.rendered;
1236 var render = cell.rendered;
1237 if (!cell.is_mergeable()) {
1237 if (!cell.is_mergeable()) {
1238 return;
1238 return;
1239 }
1239 }
1240 if (index > 0) {
1240 if (index > 0) {
1241 var upper_cell = this.get_cell(index-1);
1241 var upper_cell = this.get_cell(index-1);
1242 if (!upper_cell.is_mergeable()) {
1242 if (!upper_cell.is_mergeable()) {
1243 return;
1243 return;
1244 }
1244 }
1245 var upper_text = upper_cell.get_text();
1245 var upper_text = upper_cell.get_text();
1246 var text = cell.get_text();
1246 var text = cell.get_text();
1247 if (cell instanceof codecell.CodeCell) {
1247 if (cell instanceof codecell.CodeCell) {
1248 cell.set_text(upper_text+'\n'+text);
1248 cell.set_text(upper_text+'\n'+text);
1249 } else {
1249 } else {
1250 cell.unrender(); // Must unrender before we set_text.
1250 cell.unrender(); // Must unrender before we set_text.
1251 cell.set_text(upper_text+'\n\n'+text);
1251 cell.set_text(upper_text+'\n\n'+text);
1252 if (render) {
1252 if (render) {
1253 // The rendered state of the final cell should match
1253 // The rendered state of the final cell should match
1254 // that of the original selected cell;
1254 // that of the original selected cell;
1255 cell.render();
1255 cell.render();
1256 }
1256 }
1257 }
1257 }
1258 this.delete_cell(index-1);
1258 this.delete_cell(index-1);
1259 this.select(this.find_cell_index(cell));
1259 this.select(this.find_cell_index(cell));
1260 }
1260 }
1261 };
1261 };
1262
1262
1263 /**
1263 /**
1264 * Combine the selected cell into the cell below it.
1264 * Combine the selected cell into the cell below it.
1265 *
1265 *
1266 * @method merge_cell_below
1266 * @method merge_cell_below
1267 */
1267 */
1268 Notebook.prototype.merge_cell_below = function () {
1268 Notebook.prototype.merge_cell_below = function () {
1269 var mdc = textcell.MarkdownCell;
1269 var mdc = textcell.MarkdownCell;
1270 var rc = textcell.RawCell;
1270 var rc = textcell.RawCell;
1271 var index = this.get_selected_index();
1271 var index = this.get_selected_index();
1272 var cell = this.get_cell(index);
1272 var cell = this.get_cell(index);
1273 var render = cell.rendered;
1273 var render = cell.rendered;
1274 if (!cell.is_mergeable()) {
1274 if (!cell.is_mergeable()) {
1275 return;
1275 return;
1276 }
1276 }
1277 if (index < this.ncells()-1) {
1277 if (index < this.ncells()-1) {
1278 var lower_cell = this.get_cell(index+1);
1278 var lower_cell = this.get_cell(index+1);
1279 if (!lower_cell.is_mergeable()) {
1279 if (!lower_cell.is_mergeable()) {
1280 return;
1280 return;
1281 }
1281 }
1282 var lower_text = lower_cell.get_text();
1282 var lower_text = lower_cell.get_text();
1283 var text = cell.get_text();
1283 var text = cell.get_text();
1284 if (cell instanceof codecell.CodeCell) {
1284 if (cell instanceof codecell.CodeCell) {
1285 cell.set_text(text+'\n'+lower_text);
1285 cell.set_text(text+'\n'+lower_text);
1286 } else {
1286 } else {
1287 cell.unrender(); // Must unrender before we set_text.
1287 cell.unrender(); // Must unrender before we set_text.
1288 cell.set_text(text+'\n\n'+lower_text);
1288 cell.set_text(text+'\n\n'+lower_text);
1289 if (render) {
1289 if (render) {
1290 // The rendered state of the final cell should match
1290 // The rendered state of the final cell should match
1291 // that of the original selected cell;
1291 // that of the original selected cell;
1292 cell.render();
1292 cell.render();
1293 }
1293 }
1294 }
1294 }
1295 this.delete_cell(index+1);
1295 this.delete_cell(index+1);
1296 this.select(this.find_cell_index(cell));
1296 this.select(this.find_cell_index(cell));
1297 }
1297 }
1298 };
1298 };
1299
1299
1300
1300
1301 // Cell collapsing and output clearing
1301 // Cell collapsing and output clearing
1302
1302
1303 /**
1303 /**
1304 * Hide a cell's output.
1304 * Hide a cell's output.
1305 *
1305 *
1306 * @method collapse_output
1306 * @method collapse_output
1307 * @param {Number} index A cell's numeric index
1307 * @param {Number} index A cell's numeric index
1308 */
1308 */
1309 Notebook.prototype.collapse_output = function (index) {
1309 Notebook.prototype.collapse_output = function (index) {
1310 var i = this.index_or_selected(index);
1310 var i = this.index_or_selected(index);
1311 var cell = this.get_cell(i);
1311 var cell = this.get_cell(i);
1312 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1312 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1313 cell.collapse_output();
1313 cell.collapse_output();
1314 this.set_dirty(true);
1314 this.set_dirty(true);
1315 }
1315 }
1316 };
1316 };
1317
1317
1318 /**
1318 /**
1319 * Hide each code cell's output area.
1319 * Hide each code cell's output area.
1320 *
1320 *
1321 * @method collapse_all_output
1321 * @method collapse_all_output
1322 */
1322 */
1323 Notebook.prototype.collapse_all_output = function () {
1323 Notebook.prototype.collapse_all_output = function () {
1324 $.map(this.get_cells(), function (cell, i) {
1324 $.map(this.get_cells(), function (cell, i) {
1325 if (cell instanceof codecell.CodeCell) {
1325 if (cell instanceof codecell.CodeCell) {
1326 cell.collapse_output();
1326 cell.collapse_output();
1327 }
1327 }
1328 });
1328 });
1329 // this should not be set if the `collapse` key is removed from nbformat
1329 // this should not be set if the `collapse` key is removed from nbformat
1330 this.set_dirty(true);
1330 this.set_dirty(true);
1331 };
1331 };
1332
1332
1333 /**
1333 /**
1334 * Show a cell's output.
1334 * Show a cell's output.
1335 *
1335 *
1336 * @method expand_output
1336 * @method expand_output
1337 * @param {Number} index A cell's numeric index
1337 * @param {Number} index A cell's numeric index
1338 */
1338 */
1339 Notebook.prototype.expand_output = function (index) {
1339 Notebook.prototype.expand_output = function (index) {
1340 var i = this.index_or_selected(index);
1340 var i = this.index_or_selected(index);
1341 var cell = this.get_cell(i);
1341 var cell = this.get_cell(i);
1342 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1342 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1343 cell.expand_output();
1343 cell.expand_output();
1344 this.set_dirty(true);
1344 this.set_dirty(true);
1345 }
1345 }
1346 };
1346 };
1347
1347
1348 /**
1348 /**
1349 * Expand each code cell's output area, and remove scrollbars.
1349 * Expand each code cell's output area, and remove scrollbars.
1350 *
1350 *
1351 * @method expand_all_output
1351 * @method expand_all_output
1352 */
1352 */
1353 Notebook.prototype.expand_all_output = function () {
1353 Notebook.prototype.expand_all_output = function () {
1354 $.map(this.get_cells(), function (cell, i) {
1354 $.map(this.get_cells(), function (cell, i) {
1355 if (cell instanceof codecell.CodeCell) {
1355 if (cell instanceof codecell.CodeCell) {
1356 cell.expand_output();
1356 cell.expand_output();
1357 }
1357 }
1358 });
1358 });
1359 // this should not be set if the `collapse` key is removed from nbformat
1359 // this should not be set if the `collapse` key is removed from nbformat
1360 this.set_dirty(true);
1360 this.set_dirty(true);
1361 };
1361 };
1362
1362
1363 /**
1363 /**
1364 * Clear the selected CodeCell's output area.
1364 * Clear the selected CodeCell's output area.
1365 *
1365 *
1366 * @method clear_output
1366 * @method clear_output
1367 * @param {Number} index A cell's numeric index
1367 * @param {Number} index A cell's numeric index
1368 */
1368 */
1369 Notebook.prototype.clear_output = function (index) {
1369 Notebook.prototype.clear_output = function (index) {
1370 var i = this.index_or_selected(index);
1370 var i = this.index_or_selected(index);
1371 var cell = this.get_cell(i);
1371 var cell = this.get_cell(i);
1372 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1372 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1373 cell.clear_output();
1373 cell.clear_output();
1374 this.set_dirty(true);
1374 this.set_dirty(true);
1375 }
1375 }
1376 };
1376 };
1377
1377
1378 /**
1378 /**
1379 * Clear each code cell's output area.
1379 * Clear each code cell's output area.
1380 *
1380 *
1381 * @method clear_all_output
1381 * @method clear_all_output
1382 */
1382 */
1383 Notebook.prototype.clear_all_output = function () {
1383 Notebook.prototype.clear_all_output = function () {
1384 $.map(this.get_cells(), function (cell, i) {
1384 $.map(this.get_cells(), function (cell, i) {
1385 if (cell instanceof codecell.CodeCell) {
1385 if (cell instanceof codecell.CodeCell) {
1386 cell.clear_output();
1386 cell.clear_output();
1387 }
1387 }
1388 });
1388 });
1389 this.set_dirty(true);
1389 this.set_dirty(true);
1390 };
1390 };
1391
1391
1392 /**
1392 /**
1393 * Scroll the selected CodeCell's output area.
1393 * Scroll the selected CodeCell's output area.
1394 *
1394 *
1395 * @method scroll_output
1395 * @method scroll_output
1396 * @param {Number} index A cell's numeric index
1396 * @param {Number} index A cell's numeric index
1397 */
1397 */
1398 Notebook.prototype.scroll_output = function (index) {
1398 Notebook.prototype.scroll_output = function (index) {
1399 var i = this.index_or_selected(index);
1399 var i = this.index_or_selected(index);
1400 var cell = this.get_cell(i);
1400 var cell = this.get_cell(i);
1401 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1401 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1402 cell.scroll_output();
1402 cell.scroll_output();
1403 this.set_dirty(true);
1403 this.set_dirty(true);
1404 }
1404 }
1405 };
1405 };
1406
1406
1407 /**
1407 /**
1408 * Expand each code cell's output area, and add a scrollbar for long output.
1408 * Expand each code cell's output area, and add a scrollbar for long output.
1409 *
1409 *
1410 * @method scroll_all_output
1410 * @method scroll_all_output
1411 */
1411 */
1412 Notebook.prototype.scroll_all_output = function () {
1412 Notebook.prototype.scroll_all_output = function () {
1413 $.map(this.get_cells(), function (cell, i) {
1413 $.map(this.get_cells(), function (cell, i) {
1414 if (cell instanceof codecell.CodeCell) {
1414 if (cell instanceof codecell.CodeCell) {
1415 cell.scroll_output();
1415 cell.scroll_output();
1416 }
1416 }
1417 });
1417 });
1418 // this should not be set if the `collapse` key is removed from nbformat
1418 // this should not be set if the `collapse` key is removed from nbformat
1419 this.set_dirty(true);
1419 this.set_dirty(true);
1420 };
1420 };
1421
1421
1422 /** Toggle whether a cell's output is collapsed or expanded.
1422 /** Toggle whether a cell's output is collapsed or expanded.
1423 *
1423 *
1424 * @method toggle_output
1424 * @method toggle_output
1425 * @param {Number} index A cell's numeric index
1425 * @param {Number} index A cell's numeric index
1426 */
1426 */
1427 Notebook.prototype.toggle_output = function (index) {
1427 Notebook.prototype.toggle_output = function (index) {
1428 var i = this.index_or_selected(index);
1428 var i = this.index_or_selected(index);
1429 var cell = this.get_cell(i);
1429 var cell = this.get_cell(i);
1430 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1430 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1431 cell.toggle_output();
1431 cell.toggle_output();
1432 this.set_dirty(true);
1432 this.set_dirty(true);
1433 }
1433 }
1434 };
1434 };
1435
1435
1436 /**
1436 /**
1437 * Hide/show the output of all cells.
1437 * Hide/show the output of all cells.
1438 *
1438 *
1439 * @method toggle_all_output
1439 * @method toggle_all_output
1440 */
1440 */
1441 Notebook.prototype.toggle_all_output = function () {
1441 Notebook.prototype.toggle_all_output = function () {
1442 $.map(this.get_cells(), function (cell, i) {
1442 $.map(this.get_cells(), function (cell, i) {
1443 if (cell instanceof codecell.CodeCell) {
1443 if (cell instanceof codecell.CodeCell) {
1444 cell.toggle_output();
1444 cell.toggle_output();
1445 }
1445 }
1446 });
1446 });
1447 // this should not be set if the `collapse` key is removed from nbformat
1447 // this should not be set if the `collapse` key is removed from nbformat
1448 this.set_dirty(true);
1448 this.set_dirty(true);
1449 };
1449 };
1450
1450
1451 /**
1451 /**
1452 * Toggle a scrollbar for long cell outputs.
1452 * Toggle a scrollbar for long cell outputs.
1453 *
1453 *
1454 * @method toggle_output_scroll
1454 * @method toggle_output_scroll
1455 * @param {Number} index A cell's numeric index
1455 * @param {Number} index A cell's numeric index
1456 */
1456 */
1457 Notebook.prototype.toggle_output_scroll = function (index) {
1457 Notebook.prototype.toggle_output_scroll = function (index) {
1458 var i = this.index_or_selected(index);
1458 var i = this.index_or_selected(index);
1459 var cell = this.get_cell(i);
1459 var cell = this.get_cell(i);
1460 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1460 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1461 cell.toggle_output_scroll();
1461 cell.toggle_output_scroll();
1462 this.set_dirty(true);
1462 this.set_dirty(true);
1463 }
1463 }
1464 };
1464 };
1465
1465
1466 /**
1466 /**
1467 * Toggle the scrolling of long output on all cells.
1467 * Toggle the scrolling of long output on all cells.
1468 *
1468 *
1469 * @method toggle_all_output_scrolling
1469 * @method toggle_all_output_scrolling
1470 */
1470 */
1471 Notebook.prototype.toggle_all_output_scroll = function () {
1471 Notebook.prototype.toggle_all_output_scroll = function () {
1472 $.map(this.get_cells(), function (cell, i) {
1472 $.map(this.get_cells(), function (cell, i) {
1473 if (cell instanceof codecell.CodeCell) {
1473 if (cell instanceof codecell.CodeCell) {
1474 cell.toggle_output_scroll();
1474 cell.toggle_output_scroll();
1475 }
1475 }
1476 });
1476 });
1477 // this should not be set if the `collapse` key is removed from nbformat
1477 // this should not be set if the `collapse` key is removed from nbformat
1478 this.set_dirty(true);
1478 this.set_dirty(true);
1479 };
1479 };
1480
1480
1481 // Other cell functions: line numbers, ...
1481 // Other cell functions: line numbers, ...
1482
1482
1483 /**
1483 /**
1484 * Toggle line numbers in the selected cell's input area.
1484 * Toggle line numbers in the selected cell's input area.
1485 *
1485 *
1486 * @method cell_toggle_line_numbers
1486 * @method cell_toggle_line_numbers
1487 */
1487 */
1488 Notebook.prototype.cell_toggle_line_numbers = function() {
1488 Notebook.prototype.cell_toggle_line_numbers = function() {
1489 this.get_selected_cell().toggle_line_numbers();
1489 this.get_selected_cell().toggle_line_numbers();
1490 };
1490 };
1491
1491
1492 /**
1492 /**
1493 * Set the codemirror mode for all code cells, including the default for
1493 * Set the codemirror mode for all code cells, including the default for
1494 * new code cells.
1494 * new code cells.
1495 *
1495 *
1496 * @method set_codemirror_mode
1496 * @method set_codemirror_mode
1497 */
1497 */
1498 Notebook.prototype.set_codemirror_mode = function(newmode){
1498 Notebook.prototype.set_codemirror_mode = function(newmode){
1499 if (newmode === this.codemirror_mode) {
1499 if (newmode === this.codemirror_mode) {
1500 return;
1500 return;
1501 }
1501 }
1502 this.codemirror_mode = newmode;
1502 this.codemirror_mode = newmode;
1503 codecell.CodeCell.options_default.cm_config.mode = newmode;
1503 codecell.CodeCell.options_default.cm_config.mode = newmode;
1504 modename = newmode.name || newmode
1504 modename = newmode.name || newmode
1505
1505
1506 that = this;
1506 that = this;
1507 CodeMirror.requireMode(modename, function(){
1507 CodeMirror.requireMode(modename, function(){
1508 $.map(that.get_cells(), function(cell, i) {
1508 $.map(that.get_cells(), function(cell, i) {
1509 if (cell.cell_type === 'code'){
1509 if (cell.cell_type === 'code'){
1510 cell.code_mirror.setOption('mode', newmode);
1510 cell.code_mirror.setOption('mode', newmode);
1511 // This is currently redundant, because cm_config ends up as
1511 // This is currently redundant, because cm_config ends up as
1512 // codemirror's own .options object, but I don't want to
1512 // codemirror's own .options object, but I don't want to
1513 // rely on that.
1513 // rely on that.
1514 cell.cm_config.mode = newmode;
1514 cell.cm_config.mode = newmode;
1515 }
1515 }
1516 });
1516 });
1517 })
1517 })
1518 };
1518 };
1519
1519
1520 // Session related things
1520 // Session related things
1521
1521
1522 /**
1522 /**
1523 * Start a new session and set it on each code cell.
1523 * Start a new session and set it on each code cell.
1524 *
1524 *
1525 * @method start_session
1525 * @method start_session
1526 */
1526 */
1527 Notebook.prototype.start_session = function (kernel_name) {
1527 Notebook.prototype.start_session = function (kernel_name) {
1528 if (kernel_name === undefined) {
1528 if (kernel_name === undefined) {
1529 kernel_name = this.default_kernel_name;
1529 kernel_name = this.default_kernel_name;
1530 }
1530 }
1531 this.session = new session.Session({
1531 this.session = new session.Session({
1532 base_url: this.base_url,
1532 base_url: this.base_url,
1533 ws_url: this.ws_url,
1533 ws_url: this.ws_url,
1534 notebook_path: this.notebook_path,
1534 notebook_path: this.notebook_path,
1535 notebook_name: this.notebook_name,
1535 notebook_name: this.notebook_name,
1536 // For now, create all sessions with the 'python' kernel, which is the
1536 // For now, create all sessions with the 'python' kernel, which is the
1537 // default. Later, the user will be able to select kernels. This is
1537 // default. Later, the user will be able to select kernels. This is
1538 // overridden if KernelManager.kernel_cmd is specified for the server.
1538 // overridden if KernelManager.kernel_cmd is specified for the server.
1539 kernel_name: kernel_name,
1539 kernel_name: kernel_name,
1540 notebook: this});
1540 notebook: this});
1541
1541
1542 this.session.start($.proxy(this._session_started, this));
1542 this.session.start($.proxy(this._session_started, this));
1543 };
1543 };
1544
1544
1545
1545
1546 /**
1546 /**
1547 * Once a session is started, link the code cells to the kernel and pass the
1547 * Once a session is started, link the code cells to the kernel and pass the
1548 * comm manager to the widget manager
1548 * comm manager to the widget manager
1549 *
1549 *
1550 */
1550 */
1551 Notebook.prototype._session_started = function(){
1551 Notebook.prototype._session_started = function(){
1552 this.kernel = this.session.kernel;
1552 this.kernel = this.session.kernel;
1553 var ncells = this.ncells();
1553 var ncells = this.ncells();
1554 for (var i=0; i<ncells; i++) {
1554 for (var i=0; i<ncells; i++) {
1555 var cell = this.get_cell(i);
1555 var cell = this.get_cell(i);
1556 if (cell instanceof codecell.CodeCell) {
1556 if (cell instanceof codecell.CodeCell) {
1557 cell.set_kernel(this.session.kernel);
1557 cell.set_kernel(this.session.kernel);
1558 }
1558 }
1559 }
1559 }
1560 };
1560 };
1561
1561
1562 /**
1562 /**
1563 * Prompt the user to restart the IPython kernel.
1563 * Prompt the user to restart the IPython kernel.
1564 *
1564 *
1565 * @method restart_kernel
1565 * @method restart_kernel
1566 */
1566 */
1567 Notebook.prototype.restart_kernel = function () {
1567 Notebook.prototype.restart_kernel = function () {
1568 var that = this;
1568 var that = this;
1569 dialog.modal({
1569 dialog.modal({
1570 notebook: this,
1570 notebook: this,
1571 keyboard_manager: this.keyboard_manager,
1571 keyboard_manager: this.keyboard_manager,
1572 title : "Restart kernel or continue running?",
1572 title : "Restart kernel or continue running?",
1573 body : $("<p/>").text(
1573 body : $("<p/>").text(
1574 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1574 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1575 ),
1575 ),
1576 buttons : {
1576 buttons : {
1577 "Continue running" : {},
1577 "Continue running" : {},
1578 "Restart" : {
1578 "Restart" : {
1579 "class" : "btn-danger",
1579 "class" : "btn-danger",
1580 "click" : function() {
1580 "click" : function() {
1581 that.session.restart_kernel();
1581 that.session.restart_kernel();
1582 }
1582 }
1583 }
1583 }
1584 }
1584 }
1585 });
1585 });
1586 };
1586 };
1587
1587
1588 /**
1588 /**
1589 * Execute or render cell outputs and go into command mode.
1589 * Execute or render cell outputs and go into command mode.
1590 *
1590 *
1591 * @method execute_cell
1591 * @method execute_cell
1592 */
1592 */
1593 Notebook.prototype.execute_cell = function () {
1593 Notebook.prototype.execute_cell = function () {
1594 // mode = shift, ctrl, alt
1594 // mode = shift, ctrl, alt
1595 var cell = this.get_selected_cell();
1595 var cell = this.get_selected_cell();
1596 var cell_index = this.find_cell_index(cell);
1596 var cell_index = this.find_cell_index(cell);
1597
1597
1598 cell.execute();
1598 cell.execute();
1599 this.command_mode();
1599 this.command_mode();
1600 this.set_dirty(true);
1600 this.set_dirty(true);
1601 };
1601 };
1602
1602
1603 /**
1603 /**
1604 * Execute or render cell outputs and insert a new cell below.
1604 * Execute or render cell outputs and insert a new cell below.
1605 *
1605 *
1606 * @method execute_cell_and_insert_below
1606 * @method execute_cell_and_insert_below
1607 */
1607 */
1608 Notebook.prototype.execute_cell_and_insert_below = function () {
1608 Notebook.prototype.execute_cell_and_insert_below = function () {
1609 var cell = this.get_selected_cell();
1609 var cell = this.get_selected_cell();
1610 var cell_index = this.find_cell_index(cell);
1610 var cell_index = this.find_cell_index(cell);
1611
1611
1612 cell.execute();
1612 cell.execute();
1613
1613
1614 // If we are at the end always insert a new cell and return
1614 // If we are at the end always insert a new cell and return
1615 if (cell_index === (this.ncells()-1)) {
1615 if (cell_index === (this.ncells()-1)) {
1616 this.command_mode();
1616 this.command_mode();
1617 this.insert_cell_below();
1617 this.insert_cell_below();
1618 this.select(cell_index+1);
1618 this.select(cell_index+1);
1619 this.edit_mode();
1619 this.edit_mode();
1620 this.scroll_to_bottom();
1620 this.scroll_to_bottom();
1621 this.set_dirty(true);
1621 this.set_dirty(true);
1622 return;
1622 return;
1623 }
1623 }
1624
1624
1625 this.command_mode();
1625 this.command_mode();
1626 this.insert_cell_below();
1626 this.insert_cell_below();
1627 this.select(cell_index+1);
1627 this.select(cell_index+1);
1628 this.edit_mode();
1628 this.edit_mode();
1629 this.set_dirty(true);
1629 this.set_dirty(true);
1630 };
1630 };
1631
1631
1632 /**
1632 /**
1633 * Execute or render cell outputs and select the next cell.
1633 * Execute or render cell outputs and select the next cell.
1634 *
1634 *
1635 * @method execute_cell_and_select_below
1635 * @method execute_cell_and_select_below
1636 */
1636 */
1637 Notebook.prototype.execute_cell_and_select_below = function () {
1637 Notebook.prototype.execute_cell_and_select_below = function () {
1638
1638
1639 var cell = this.get_selected_cell();
1639 var cell = this.get_selected_cell();
1640 var cell_index = this.find_cell_index(cell);
1640 var cell_index = this.find_cell_index(cell);
1641
1641
1642 cell.execute();
1642 cell.execute();
1643
1643
1644 // If we are at the end always insert a new cell and return
1644 // If we are at the end always insert a new cell and return
1645 if (cell_index === (this.ncells()-1)) {
1645 if (cell_index === (this.ncells()-1)) {
1646 this.command_mode();
1646 this.command_mode();
1647 this.insert_cell_below();
1647 this.insert_cell_below();
1648 this.select(cell_index+1);
1648 this.select(cell_index+1);
1649 this.edit_mode();
1649 this.edit_mode();
1650 this.scroll_to_bottom();
1650 this.scroll_to_bottom();
1651 this.set_dirty(true);
1651 this.set_dirty(true);
1652 return;
1652 return;
1653 }
1653 }
1654
1654
1655 this.command_mode();
1655 this.command_mode();
1656 this.select(cell_index+1);
1656 this.select(cell_index+1);
1657 this.focus_cell();
1657 this.focus_cell();
1658 this.set_dirty(true);
1658 this.set_dirty(true);
1659 };
1659 };
1660
1660
1661 /**
1661 /**
1662 * Execute all cells below the selected cell.
1662 * Execute all cells below the selected cell.
1663 *
1663 *
1664 * @method execute_cells_below
1664 * @method execute_cells_below
1665 */
1665 */
1666 Notebook.prototype.execute_cells_below = function () {
1666 Notebook.prototype.execute_cells_below = function () {
1667 this.execute_cell_range(this.get_selected_index(), this.ncells());
1667 this.execute_cell_range(this.get_selected_index(), this.ncells());
1668 this.scroll_to_bottom();
1668 this.scroll_to_bottom();
1669 };
1669 };
1670
1670
1671 /**
1671 /**
1672 * Execute all cells above the selected cell.
1672 * Execute all cells above the selected cell.
1673 *
1673 *
1674 * @method execute_cells_above
1674 * @method execute_cells_above
1675 */
1675 */
1676 Notebook.prototype.execute_cells_above = function () {
1676 Notebook.prototype.execute_cells_above = function () {
1677 this.execute_cell_range(0, this.get_selected_index());
1677 this.execute_cell_range(0, this.get_selected_index());
1678 };
1678 };
1679
1679
1680 /**
1680 /**
1681 * Execute all cells.
1681 * Execute all cells.
1682 *
1682 *
1683 * @method execute_all_cells
1683 * @method execute_all_cells
1684 */
1684 */
1685 Notebook.prototype.execute_all_cells = function () {
1685 Notebook.prototype.execute_all_cells = function () {
1686 this.execute_cell_range(0, this.ncells());
1686 this.execute_cell_range(0, this.ncells());
1687 this.scroll_to_bottom();
1687 this.scroll_to_bottom();
1688 };
1688 };
1689
1689
1690 /**
1690 /**
1691 * Execute a contiguous range of cells.
1691 * Execute a contiguous range of cells.
1692 *
1692 *
1693 * @method execute_cell_range
1693 * @method execute_cell_range
1694 * @param {Number} start Index of the first cell to execute (inclusive)
1694 * @param {Number} start Index of the first cell to execute (inclusive)
1695 * @param {Number} end Index of the last cell to execute (exclusive)
1695 * @param {Number} end Index of the last cell to execute (exclusive)
1696 */
1696 */
1697 Notebook.prototype.execute_cell_range = function (start, end) {
1697 Notebook.prototype.execute_cell_range = function (start, end) {
1698 this.command_mode();
1698 this.command_mode();
1699 for (var i=start; i<end; i++) {
1699 for (var i=start; i<end; i++) {
1700 this.select(i);
1700 this.select(i);
1701 this.execute_cell();
1701 this.execute_cell();
1702 }
1702 }
1703 };
1703 };
1704
1704
1705 // Persistance and loading
1705 // Persistance and loading
1706
1706
1707 /**
1707 /**
1708 * Getter method for this notebook's name.
1708 * Getter method for this notebook's name.
1709 *
1709 *
1710 * @method get_notebook_name
1710 * @method get_notebook_name
1711 * @return {String} This notebook's name (excluding file extension)
1711 * @return {String} This notebook's name (excluding file extension)
1712 */
1712 */
1713 Notebook.prototype.get_notebook_name = function () {
1713 Notebook.prototype.get_notebook_name = function () {
1714 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1714 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1715 return nbname;
1715 return nbname;
1716 };
1716 };
1717
1717
1718 /**
1718 /**
1719 * Setter method for this notebook's name.
1719 * Setter method for this notebook's name.
1720 *
1720 *
1721 * @method set_notebook_name
1721 * @method set_notebook_name
1722 * @param {String} name A new name for this notebook
1722 * @param {String} name A new name for this notebook
1723 */
1723 */
1724 Notebook.prototype.set_notebook_name = function (name) {
1724 Notebook.prototype.set_notebook_name = function (name) {
1725 this.notebook_name = name;
1725 this.notebook_name = name;
1726 };
1726 };
1727
1727
1728 /**
1728 /**
1729 * Check that a notebook's name is valid.
1729 * Check that a notebook's name is valid.
1730 *
1730 *
1731 * @method test_notebook_name
1731 * @method test_notebook_name
1732 * @param {String} nbname A name for this notebook
1732 * @param {String} nbname A name for this notebook
1733 * @return {Boolean} True if the name is valid, false if invalid
1733 * @return {Boolean} True if the name is valid, false if invalid
1734 */
1734 */
1735 Notebook.prototype.test_notebook_name = function (nbname) {
1735 Notebook.prototype.test_notebook_name = function (nbname) {
1736 nbname = nbname || '';
1736 nbname = nbname || '';
1737 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1737 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1738 return true;
1738 return true;
1739 } else {
1739 } else {
1740 return false;
1740 return false;
1741 }
1741 }
1742 };
1742 };
1743
1743
1744 /**
1744 /**
1745 * Load a notebook from JSON (.ipynb).
1745 * Load a notebook from JSON (.ipynb).
1746 *
1746 *
1747 * This currently handles one worksheet: others are deleted.
1747 * This currently handles one worksheet: others are deleted.
1748 *
1748 *
1749 * @method fromJSON
1749 * @method fromJSON
1750 * @param {Object} data JSON representation of a notebook
1750 * @param {Object} data JSON representation of a notebook
1751 */
1751 */
1752 Notebook.prototype.fromJSON = function (data) {
1752 Notebook.prototype.fromJSON = function (data) {
1753 var content = data.content;
1753 var content = data.content;
1754 var ncells = this.ncells();
1754 var ncells = this.ncells();
1755 var i;
1755 var i;
1756 for (i=0; i<ncells; i++) {
1756 for (i=0; i<ncells; i++) {
1757 // Always delete cell 0 as they get renumbered as they are deleted.
1757 // Always delete cell 0 as they get renumbered as they are deleted.
1758 this.delete_cell(0);
1758 this.delete_cell(0);
1759 }
1759 }
1760 // Save the metadata and name.
1760 // Save the metadata and name.
1761 this.metadata = content.metadata;
1761 this.metadata = content.metadata;
1762 this.notebook_name = data.name;
1762 this.notebook_name = data.name;
1763 var trusted = true;
1763 var trusted = true;
1764
1764
1765 // Trigger an event changing the kernel spec - this will set the default
1765 // Trigger an event changing the kernel spec - this will set the default
1766 // codemirror mode
1766 // codemirror mode
1767 if (this.metadata.kernelspec !== undefined) {
1767 if (this.metadata.kernelspec !== undefined) {
1768 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1768 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1769 }
1769 }
1770
1770
1771 // Only handle 1 worksheet for now.
1771 // Only handle 1 worksheet for now.
1772 var worksheet = content.worksheets[0];
1772 var worksheet = content.worksheets[0];
1773 if (worksheet !== undefined) {
1773 if (worksheet !== undefined) {
1774 if (worksheet.metadata) {
1774 if (worksheet.metadata) {
1775 this.worksheet_metadata = worksheet.metadata;
1775 this.worksheet_metadata = worksheet.metadata;
1776 }
1776 }
1777 var new_cells = worksheet.cells;
1777 var new_cells = worksheet.cells;
1778 ncells = new_cells.length;
1778 ncells = new_cells.length;
1779 var cell_data = null;
1779 var cell_data = null;
1780 var new_cell = null;
1780 var new_cell = null;
1781 for (i=0; i<ncells; i++) {
1781 for (i=0; i<ncells; i++) {
1782 cell_data = new_cells[i];
1782 cell_data = new_cells[i];
1783 // VERSIONHACK: plaintext -> raw
1783 // VERSIONHACK: plaintext -> raw
1784 // handle never-released plaintext name for raw cells
1784 // handle never-released plaintext name for raw cells
1785 if (cell_data.cell_type === 'plaintext'){
1785 if (cell_data.cell_type === 'plaintext'){
1786 cell_data.cell_type = 'raw';
1786 cell_data.cell_type = 'raw';
1787 }
1787 }
1788
1788
1789 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1789 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1790 new_cell.fromJSON(cell_data);
1790 new_cell.fromJSON(cell_data);
1791 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1791 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1792 trusted = false;
1792 trusted = false;
1793 }
1793 }
1794 }
1794 }
1795 }
1795 }
1796 if (trusted != this.trusted) {
1796 if (trusted != this.trusted) {
1797 this.trusted = trusted;
1797 this.trusted = trusted;
1798 this.events.trigger("trust_changed.Notebook", trusted);
1798 this.events.trigger("trust_changed.Notebook", trusted);
1799 }
1799 }
1800 if (content.worksheets.length > 1) {
1800 if (content.worksheets.length > 1) {
1801 dialog.modal({
1801 dialog.modal({
1802 notebook: this,
1802 notebook: this,
1803 keyboard_manager: this.keyboard_manager,
1803 keyboard_manager: this.keyboard_manager,
1804 title : "Multiple worksheets",
1804 title : "Multiple worksheets",
1805 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1805 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1806 "but this version of IPython can only handle the first. " +
1806 "but this version of IPython can only handle the first. " +
1807 "If you save this notebook, worksheets after the first will be lost.",
1807 "If you save this notebook, worksheets after the first will be lost.",
1808 buttons : {
1808 buttons : {
1809 OK : {
1809 OK : {
1810 class : "btn-danger"
1810 class : "btn-danger"
1811 }
1811 }
1812 }
1812 }
1813 });
1813 });
1814 }
1814 }
1815 };
1815 };
1816
1816
1817 /**
1817 /**
1818 * Dump this notebook into a JSON-friendly object.
1818 * Dump this notebook into a JSON-friendly object.
1819 *
1819 *
1820 * @method toJSON
1820 * @method toJSON
1821 * @return {Object} A JSON-friendly representation of this notebook.
1821 * @return {Object} A JSON-friendly representation of this notebook.
1822 */
1822 */
1823 Notebook.prototype.toJSON = function () {
1823 Notebook.prototype.toJSON = function () {
1824 var cells = this.get_cells();
1824 var cells = this.get_cells();
1825 var ncells = cells.length;
1825 var ncells = cells.length;
1826 var cell_array = new Array(ncells);
1826 var cell_array = new Array(ncells);
1827 var trusted = true;
1827 var trusted = true;
1828 for (var i=0; i<ncells; i++) {
1828 for (var i=0; i<ncells; i++) {
1829 var cell = cells[i];
1829 var cell = cells[i];
1830 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1830 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1831 trusted = false;
1831 trusted = false;
1832 }
1832 }
1833 cell_array[i] = cell.toJSON();
1833 cell_array[i] = cell.toJSON();
1834 }
1834 }
1835 var data = {
1835 var data = {
1836 // Only handle 1 worksheet for now.
1836 // Only handle 1 worksheet for now.
1837 worksheets : [{
1837 worksheets : [{
1838 cells: cell_array,
1838 cells: cell_array,
1839 metadata: this.worksheet_metadata
1839 metadata: this.worksheet_metadata
1840 }],
1840 }],
1841 metadata : this.metadata
1841 metadata : this.metadata
1842 };
1842 };
1843 if (trusted != this.trusted) {
1843 if (trusted != this.trusted) {
1844 this.trusted = trusted;
1844 this.trusted = trusted;
1845 this.events.trigger("trust_changed.Notebook", trusted);
1845 this.events.trigger("trust_changed.Notebook", trusted);
1846 }
1846 }
1847 return data;
1847 return data;
1848 };
1848 };
1849
1849
1850 /**
1850 /**
1851 * Start an autosave timer, for periodically saving the notebook.
1851 * Start an autosave timer, for periodically saving the notebook.
1852 *
1852 *
1853 * @method set_autosave_interval
1853 * @method set_autosave_interval
1854 * @param {Integer} interval the autosave interval in milliseconds
1854 * @param {Integer} interval the autosave interval in milliseconds
1855 */
1855 */
1856 Notebook.prototype.set_autosave_interval = function (interval) {
1856 Notebook.prototype.set_autosave_interval = function (interval) {
1857 var that = this;
1857 var that = this;
1858 // clear previous interval, so we don't get simultaneous timers
1858 // clear previous interval, so we don't get simultaneous timers
1859 if (this.autosave_timer) {
1859 if (this.autosave_timer) {
1860 clearInterval(this.autosave_timer);
1860 clearInterval(this.autosave_timer);
1861 }
1861 }
1862
1862
1863 this.autosave_interval = this.minimum_autosave_interval = interval;
1863 this.autosave_interval = this.minimum_autosave_interval = interval;
1864 if (interval) {
1864 if (interval) {
1865 this.autosave_timer = setInterval(function() {
1865 this.autosave_timer = setInterval(function() {
1866 if (that.dirty) {
1866 if (that.dirty) {
1867 that.save_notebook();
1867 that.save_notebook();
1868 }
1868 }
1869 }, interval);
1869 }, interval);
1870 this.events.trigger("autosave_enabled.Notebook", interval);
1870 this.events.trigger("autosave_enabled.Notebook", interval);
1871 } else {
1871 } else {
1872 this.autosave_timer = null;
1872 this.autosave_timer = null;
1873 this.events.trigger("autosave_disabled.Notebook");
1873 this.events.trigger("autosave_disabled.Notebook");
1874 }
1874 }
1875 };
1875 };
1876
1876
1877 /**
1877 /**
1878 * Save this notebook on the server. This becomes a notebook instance's
1878 * Save this notebook on the server. This becomes a notebook instance's
1879 * .save_notebook method *after* the entire notebook has been loaded.
1879 * .save_notebook method *after* the entire notebook has been loaded.
1880 *
1880 *
1881 * @method save_notebook
1881 * @method save_notebook
1882 */
1882 */
1883 Notebook.prototype.save_notebook = function (extra_settings) {
1883 Notebook.prototype.save_notebook = function (extra_settings) {
1884 // Create a JSON model to be sent to the server.
1884 // Create a JSON model to be sent to the server.
1885 var model = {};
1885 var model = {};
1886 model.name = this.notebook_name;
1886 model.name = this.notebook_name;
1887 model.path = this.notebook_path;
1887 model.path = this.notebook_path;
1888 model.content = this.toJSON();
1888 model.content = this.toJSON();
1889 model.content.nbformat = this.nbformat;
1889 model.content.nbformat = this.nbformat;
1890 model.content.nbformat_minor = this.nbformat_minor;
1890 model.content.nbformat_minor = this.nbformat_minor;
1891 // time the ajax call for autosave tuning purposes.
1891 // time the ajax call for autosave tuning purposes.
1892 var start = new Date().getTime();
1892 var start = new Date().getTime();
1893 // We do the call with settings so we can set cache to false.
1893 // We do the call with settings so we can set cache to false.
1894 var settings = {
1894 var settings = {
1895 processData : false,
1895 processData : false,
1896 cache : false,
1896 cache : false,
1897 type : "PUT",
1897 type : "PUT",
1898 data : JSON.stringify(model),
1898 data : JSON.stringify(model),
1899 headers : {'Content-Type': 'application/json'},
1899 headers : {'Content-Type': 'application/json'},
1900 success : $.proxy(this.save_notebook_success, this, start),
1900 success : $.proxy(this.save_notebook_success, this, start),
1901 error : $.proxy(this.save_notebook_error, this)
1901 error : $.proxy(this.save_notebook_error, this)
1902 };
1902 };
1903 if (extra_settings) {
1903 if (extra_settings) {
1904 for (var key in extra_settings) {
1904 for (var key in extra_settings) {
1905 settings[key] = extra_settings[key];
1905 settings[key] = extra_settings[key];
1906 }
1906 }
1907 }
1907 }
1908 this.events.trigger('notebook_saving.Notebook');
1908 this.events.trigger('notebook_saving.Notebook');
1909 var url = utils.url_join_encode(
1909 var url = utils.url_join_encode(
1910 this.base_url,
1910 this.base_url,
1911 'api/notebooks',
1911 'api/contents',
1912 this.notebook_path,
1912 this.notebook_path,
1913 this.notebook_name
1913 this.notebook_name
1914 );
1914 );
1915 $.ajax(url, settings);
1915 $.ajax(url, settings);
1916 };
1916 };
1917
1917
1918 /**
1918 /**
1919 * Success callback for saving a notebook.
1919 * Success callback for saving a notebook.
1920 *
1920 *
1921 * @method save_notebook_success
1921 * @method save_notebook_success
1922 * @param {Integer} start the time when the save request started
1922 * @param {Integer} start the time when the save request started
1923 * @param {Object} data JSON representation of a notebook
1923 * @param {Object} data JSON representation of a notebook
1924 * @param {String} status Description of response status
1924 * @param {String} status Description of response status
1925 * @param {jqXHR} xhr jQuery Ajax object
1925 * @param {jqXHR} xhr jQuery Ajax object
1926 */
1926 */
1927 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1927 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1928 this.set_dirty(false);
1928 this.set_dirty(false);
1929 this.events.trigger('notebook_saved.Notebook');
1929 this.events.trigger('notebook_saved.Notebook');
1930 this._update_autosave_interval(start);
1930 this._update_autosave_interval(start);
1931 if (this._checkpoint_after_save) {
1931 if (this._checkpoint_after_save) {
1932 this.create_checkpoint();
1932 this.create_checkpoint();
1933 this._checkpoint_after_save = false;
1933 this._checkpoint_after_save = false;
1934 }
1934 }
1935 };
1935 };
1936
1936
1937 /**
1937 /**
1938 * update the autosave interval based on how long the last save took
1938 * update the autosave interval based on how long the last save took
1939 *
1939 *
1940 * @method _update_autosave_interval
1940 * @method _update_autosave_interval
1941 * @param {Integer} timestamp when the save request started
1941 * @param {Integer} timestamp when the save request started
1942 */
1942 */
1943 Notebook.prototype._update_autosave_interval = function (start) {
1943 Notebook.prototype._update_autosave_interval = function (start) {
1944 var duration = (new Date().getTime() - start);
1944 var duration = (new Date().getTime() - start);
1945 if (this.autosave_interval) {
1945 if (this.autosave_interval) {
1946 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1946 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1947 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1947 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1948 // round to 10 seconds, otherwise we will be setting a new interval too often
1948 // round to 10 seconds, otherwise we will be setting a new interval too often
1949 interval = 10000 * Math.round(interval / 10000);
1949 interval = 10000 * Math.round(interval / 10000);
1950 // set new interval, if it's changed
1950 // set new interval, if it's changed
1951 if (interval != this.autosave_interval) {
1951 if (interval != this.autosave_interval) {
1952 this.set_autosave_interval(interval);
1952 this.set_autosave_interval(interval);
1953 }
1953 }
1954 }
1954 }
1955 };
1955 };
1956
1956
1957 /**
1957 /**
1958 * Failure callback for saving a notebook.
1958 * Failure callback for saving a notebook.
1959 *
1959 *
1960 * @method save_notebook_error
1960 * @method save_notebook_error
1961 * @param {jqXHR} xhr jQuery Ajax object
1961 * @param {jqXHR} xhr jQuery Ajax object
1962 * @param {String} status Description of response status
1962 * @param {String} status Description of response status
1963 * @param {String} error HTTP error message
1963 * @param {String} error HTTP error message
1964 */
1964 */
1965 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1965 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1966 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1966 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1967 };
1967 };
1968
1968
1969 /**
1969 /**
1970 * Explicitly trust the output of this notebook.
1970 * Explicitly trust the output of this notebook.
1971 *
1971 *
1972 * @method trust_notebook
1972 * @method trust_notebook
1973 */
1973 */
1974 Notebook.prototype.trust_notebook = function (extra_settings) {
1974 Notebook.prototype.trust_notebook = function (extra_settings) {
1975 var body = $("<div>").append($("<p>")
1975 var body = $("<div>").append($("<p>")
1976 .text("A trusted IPython notebook may execute hidden malicious code ")
1976 .text("A trusted IPython notebook may execute hidden malicious code ")
1977 .append($("<strong>")
1977 .append($("<strong>")
1978 .append(
1978 .append(
1979 $("<em>").text("when you open it")
1979 $("<em>").text("when you open it")
1980 )
1980 )
1981 ).append(".").append(
1981 ).append(".").append(
1982 " Selecting trust will immediately reload this notebook in a trusted state."
1982 " Selecting trust will immediately reload this notebook in a trusted state."
1983 ).append(
1983 ).append(
1984 " For more information, see the "
1984 " For more information, see the "
1985 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1985 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1986 .text("IPython security documentation")
1986 .text("IPython security documentation")
1987 ).append(".")
1987 ).append(".")
1988 );
1988 );
1989
1989
1990 var nb = this;
1990 var nb = this;
1991 dialog.modal({
1991 dialog.modal({
1992 notebook: this,
1992 notebook: this,
1993 keyboard_manager: this.keyboard_manager,
1993 keyboard_manager: this.keyboard_manager,
1994 title: "Trust this notebook?",
1994 title: "Trust this notebook?",
1995 body: body,
1995 body: body,
1996
1996
1997 buttons: {
1997 buttons: {
1998 Cancel : {},
1998 Cancel : {},
1999 Trust : {
1999 Trust : {
2000 class : "btn-danger",
2000 class : "btn-danger",
2001 click : function () {
2001 click : function () {
2002 var cells = nb.get_cells();
2002 var cells = nb.get_cells();
2003 for (var i = 0; i < cells.length; i++) {
2003 for (var i = 0; i < cells.length; i++) {
2004 var cell = cells[i];
2004 var cell = cells[i];
2005 if (cell.cell_type == 'code') {
2005 if (cell.cell_type == 'code') {
2006 cell.output_area.trusted = true;
2006 cell.output_area.trusted = true;
2007 }
2007 }
2008 }
2008 }
2009 this.events.on('notebook_saved.Notebook', function () {
2009 this.events.on('notebook_saved.Notebook', function () {
2010 window.location.reload();
2010 window.location.reload();
2011 });
2011 });
2012 nb.save_notebook();
2012 nb.save_notebook();
2013 }
2013 }
2014 }
2014 }
2015 }
2015 }
2016 });
2016 });
2017 };
2017 };
2018
2018
2019 Notebook.prototype.new_notebook = function(){
2019 Notebook.prototype.new_notebook = function(){
2020 var path = this.notebook_path;
2020 var path = this.notebook_path;
2021 var base_url = this.base_url;
2021 var base_url = this.base_url;
2022 var settings = {
2022 var settings = {
2023 processData : false,
2023 processData : false,
2024 cache : false,
2024 cache : false,
2025 type : "POST",
2025 type : "POST",
2026 dataType : "json",
2026 dataType : "json",
2027 async : false,
2027 async : false,
2028 success : function (data, status, xhr){
2028 success : function (data, status, xhr){
2029 var notebook_name = data.name;
2029 var notebook_name = data.name;
2030 window.open(
2030 window.open(
2031 utils.url_join_encode(
2031 utils.url_join_encode(
2032 base_url,
2032 base_url,
2033 'notebooks',
2033 'notebooks',
2034 path,
2034 path,
2035 notebook_name
2035 notebook_name
2036 ),
2036 ),
2037 '_blank'
2037 '_blank'
2038 );
2038 );
2039 },
2039 },
2040 error : utils.log_ajax_error,
2040 error : utils.log_ajax_error,
2041 };
2041 };
2042 var url = utils.url_join_encode(
2042 var url = utils.url_join_encode(
2043 base_url,
2043 base_url,
2044 'api/notebooks',
2044 'api/contents',
2045 path
2045 path
2046 );
2046 );
2047 $.ajax(url,settings);
2047 $.ajax(url,settings);
2048 };
2048 };
2049
2049
2050
2050
2051 Notebook.prototype.copy_notebook = function(){
2051 Notebook.prototype.copy_notebook = function(){
2052 var path = this.notebook_path;
2052 var path = this.notebook_path;
2053 var base_url = this.base_url;
2053 var base_url = this.base_url;
2054 var settings = {
2054 var settings = {
2055 processData : false,
2055 processData : false,
2056 cache : false,
2056 cache : false,
2057 type : "POST",
2057 type : "POST",
2058 dataType : "json",
2058 dataType : "json",
2059 data : JSON.stringify({copy_from : this.notebook_name}),
2059 data : JSON.stringify({copy_from : this.notebook_name}),
2060 async : false,
2060 async : false,
2061 success : function (data, status, xhr) {
2061 success : function (data, status, xhr) {
2062 window.open(utils.url_join_encode(
2062 window.open(utils.url_join_encode(
2063 base_url,
2063 base_url,
2064 'notebooks',
2064 'notebooks',
2065 data.path,
2065 data.path,
2066 data.name
2066 data.name
2067 ), '_blank');
2067 ), '_blank');
2068 },
2068 },
2069 error : utils.log_ajax_error,
2069 error : utils.log_ajax_error,
2070 };
2070 };
2071 var url = utils.url_join_encode(
2071 var url = utils.url_join_encode(
2072 base_url,
2072 base_url,
2073 'api/notebooks',
2073 'api/contents',
2074 path
2074 path
2075 );
2075 );
2076 $.ajax(url,settings);
2076 $.ajax(url,settings);
2077 };
2077 };
2078
2078
2079 Notebook.prototype.rename = function (nbname) {
2079 Notebook.prototype.rename = function (nbname) {
2080 var that = this;
2080 var that = this;
2081 if (!nbname.match(/\.ipynb$/)) {
2081 if (!nbname.match(/\.ipynb$/)) {
2082 nbname = nbname + ".ipynb";
2082 nbname = nbname + ".ipynb";
2083 }
2083 }
2084 var data = {name: nbname};
2084 var data = {name: nbname};
2085 var settings = {
2085 var settings = {
2086 processData : false,
2086 processData : false,
2087 cache : false,
2087 cache : false,
2088 type : "PATCH",
2088 type : "PATCH",
2089 data : JSON.stringify(data),
2089 data : JSON.stringify(data),
2090 dataType: "json",
2090 dataType: "json",
2091 headers : {'Content-Type': 'application/json'},
2091 headers : {'Content-Type': 'application/json'},
2092 success : $.proxy(that.rename_success, this),
2092 success : $.proxy(that.rename_success, this),
2093 error : $.proxy(that.rename_error, this)
2093 error : $.proxy(that.rename_error, this)
2094 };
2094 };
2095 this.events.trigger('rename_notebook.Notebook', data);
2095 this.events.trigger('rename_notebook.Notebook', data);
2096 var url = utils.url_join_encode(
2096 var url = utils.url_join_encode(
2097 this.base_url,
2097 this.base_url,
2098 'api/notebooks',
2098 'api/contents',
2099 this.notebook_path,
2099 this.notebook_path,
2100 this.notebook_name
2100 this.notebook_name
2101 );
2101 );
2102 $.ajax(url, settings);
2102 $.ajax(url, settings);
2103 };
2103 };
2104
2104
2105 Notebook.prototype.delete = function () {
2105 Notebook.prototype.delete = function () {
2106 var that = this;
2106 var that = this;
2107 var settings = {
2107 var settings = {
2108 processData : false,
2108 processData : false,
2109 cache : false,
2109 cache : false,
2110 type : "DELETE",
2110 type : "DELETE",
2111 dataType: "json",
2111 dataType: "json",
2112 error : utils.log_ajax_error,
2112 error : utils.log_ajax_error,
2113 };
2113 };
2114 var url = utils.url_join_encode(
2114 var url = utils.url_join_encode(
2115 this.base_url,
2115 this.base_url,
2116 'api/notebooks',
2116 'api/contents',
2117 this.notebook_path,
2117 this.notebook_path,
2118 this.notebook_name
2118 this.notebook_name
2119 );
2119 );
2120 $.ajax(url, settings);
2120 $.ajax(url, settings);
2121 };
2121 };
2122
2122
2123
2123
2124 Notebook.prototype.rename_success = function (json, status, xhr) {
2124 Notebook.prototype.rename_success = function (json, status, xhr) {
2125 var name = this.notebook_name = json.name;
2125 var name = this.notebook_name = json.name;
2126 var path = json.path;
2126 var path = json.path;
2127 this.session.rename_notebook(name, path);
2127 this.session.rename_notebook(name, path);
2128 this.events.trigger('notebook_renamed.Notebook', json);
2128 this.events.trigger('notebook_renamed.Notebook', json);
2129 };
2129 };
2130
2130
2131 Notebook.prototype.rename_error = function (xhr, status, error) {
2131 Notebook.prototype.rename_error = function (xhr, status, error) {
2132 var that = this;
2132 var that = this;
2133 var dialog_body = $('<div/>').append(
2133 var dialog_body = $('<div/>').append(
2134 $("<p/>").text('This notebook name already exists.')
2134 $("<p/>").text('This notebook name already exists.')
2135 );
2135 );
2136 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2136 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2137 dialog.modal({
2137 dialog.modal({
2138 notebook: this,
2138 notebook: this,
2139 keyboard_manager: this.keyboard_manager,
2139 keyboard_manager: this.keyboard_manager,
2140 title: "Notebook Rename Error!",
2140 title: "Notebook Rename Error!",
2141 body: dialog_body,
2141 body: dialog_body,
2142 buttons : {
2142 buttons : {
2143 "Cancel": {},
2143 "Cancel": {},
2144 "OK": {
2144 "OK": {
2145 class: "btn-primary",
2145 class: "btn-primary",
2146 click: function () {
2146 click: function () {
2147 this.save_widget.rename_notebook({notebook:that});
2147 this.save_widget.rename_notebook({notebook:that});
2148 }}
2148 }}
2149 },
2149 },
2150 open : function (event, ui) {
2150 open : function (event, ui) {
2151 var that = $(this);
2151 var that = $(this);
2152 // Upon ENTER, click the OK button.
2152 // Upon ENTER, click the OK button.
2153 that.find('input[type="text"]').keydown(function (event, ui) {
2153 that.find('input[type="text"]').keydown(function (event, ui) {
2154 if (event.which === this.keyboard.keycodes.enter) {
2154 if (event.which === this.keyboard.keycodes.enter) {
2155 that.find('.btn-primary').first().click();
2155 that.find('.btn-primary').first().click();
2156 }
2156 }
2157 });
2157 });
2158 that.find('input[type="text"]').focus();
2158 that.find('input[type="text"]').focus();
2159 }
2159 }
2160 });
2160 });
2161 };
2161 };
2162
2162
2163 /**
2163 /**
2164 * Request a notebook's data from the server.
2164 * Request a notebook's data from the server.
2165 *
2165 *
2166 * @method load_notebook
2166 * @method load_notebook
2167 * @param {String} notebook_name and path A notebook to load
2167 * @param {String} notebook_name and path A notebook to load
2168 */
2168 */
2169 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2169 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2170 var that = this;
2170 var that = this;
2171 this.notebook_name = notebook_name;
2171 this.notebook_name = notebook_name;
2172 this.notebook_path = notebook_path;
2172 this.notebook_path = notebook_path;
2173 // We do the call with settings so we can set cache to false.
2173 // We do the call with settings so we can set cache to false.
2174 var settings = {
2174 var settings = {
2175 processData : false,
2175 processData : false,
2176 cache : false,
2176 cache : false,
2177 type : "GET",
2177 type : "GET",
2178 dataType : "json",
2178 dataType : "json",
2179 success : $.proxy(this.load_notebook_success,this),
2179 success : $.proxy(this.load_notebook_success,this),
2180 error : $.proxy(this.load_notebook_error,this),
2180 error : $.proxy(this.load_notebook_error,this),
2181 };
2181 };
2182 this.events.trigger('notebook_loading.Notebook');
2182 this.events.trigger('notebook_loading.Notebook');
2183 var url = utils.url_join_encode(
2183 var url = utils.url_join_encode(
2184 this.base_url,
2184 this.base_url,
2185 'api/notebooks',
2185 'api/contents',
2186 this.notebook_path,
2186 this.notebook_path,
2187 this.notebook_name
2187 this.notebook_name
2188 );
2188 );
2189 $.ajax(url, settings);
2189 $.ajax(url, settings);
2190 };
2190 };
2191
2191
2192 /**
2192 /**
2193 * Success callback for loading a notebook from the server.
2193 * Success callback for loading a notebook from the server.
2194 *
2194 *
2195 * Load notebook data from the JSON response.
2195 * Load notebook data from the JSON response.
2196 *
2196 *
2197 * @method load_notebook_success
2197 * @method load_notebook_success
2198 * @param {Object} data JSON representation of a notebook
2198 * @param {Object} data JSON representation of a notebook
2199 * @param {String} status Description of response status
2199 * @param {String} status Description of response status
2200 * @param {jqXHR} xhr jQuery Ajax object
2200 * @param {jqXHR} xhr jQuery Ajax object
2201 */
2201 */
2202 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2202 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2203 this.fromJSON(data);
2203 this.fromJSON(data);
2204 if (this.ncells() === 0) {
2204 if (this.ncells() === 0) {
2205 this.insert_cell_below('code');
2205 this.insert_cell_below('code');
2206 this.edit_mode(0);
2206 this.edit_mode(0);
2207 } else {
2207 } else {
2208 this.select(0);
2208 this.select(0);
2209 this.handle_command_mode(this.get_cell(0));
2209 this.handle_command_mode(this.get_cell(0));
2210 }
2210 }
2211 this.set_dirty(false);
2211 this.set_dirty(false);
2212 this.scroll_to_top();
2212 this.scroll_to_top();
2213 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2213 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2214 var msg = "This notebook has been converted from an older " +
2214 var msg = "This notebook has been converted from an older " +
2215 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2215 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2216 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2216 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2217 "newer notebook format will be used and older versions of IPython " +
2217 "newer notebook format will be used and older versions of IPython " +
2218 "may not be able to read it. To keep the older version, close the " +
2218 "may not be able to read it. To keep the older version, close the " +
2219 "notebook without saving it.";
2219 "notebook without saving it.";
2220 dialog.modal({
2220 dialog.modal({
2221 notebook: this,
2221 notebook: this,
2222 keyboard_manager: this.keyboard_manager,
2222 keyboard_manager: this.keyboard_manager,
2223 title : "Notebook converted",
2223 title : "Notebook converted",
2224 body : msg,
2224 body : msg,
2225 buttons : {
2225 buttons : {
2226 OK : {
2226 OK : {
2227 class : "btn-primary"
2227 class : "btn-primary"
2228 }
2228 }
2229 }
2229 }
2230 });
2230 });
2231 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2231 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2232 var that = this;
2232 var that = this;
2233 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2233 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2234 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2234 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2235 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2235 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2236 this_vs + ". You can still work with this notebook, but some features " +
2236 this_vs + ". You can still work with this notebook, but some features " +
2237 "introduced in later notebook versions may not be available.";
2237 "introduced in later notebook versions may not be available.";
2238
2238
2239 dialog.modal({
2239 dialog.modal({
2240 notebook: this,
2240 notebook: this,
2241 keyboard_manager: this.keyboard_manager,
2241 keyboard_manager: this.keyboard_manager,
2242 title : "Newer Notebook",
2242 title : "Newer Notebook",
2243 body : msg,
2243 body : msg,
2244 buttons : {
2244 buttons : {
2245 OK : {
2245 OK : {
2246 class : "btn-danger"
2246 class : "btn-danger"
2247 }
2247 }
2248 }
2248 }
2249 });
2249 });
2250
2250
2251 }
2251 }
2252
2252
2253 // Create the session after the notebook is completely loaded to prevent
2253 // Create the session after the notebook is completely loaded to prevent
2254 // code execution upon loading, which is a security risk.
2254 // code execution upon loading, which is a security risk.
2255 if (this.session === null) {
2255 if (this.session === null) {
2256 var kernelspec = this.metadata.kernelspec || {};
2256 var kernelspec = this.metadata.kernelspec || {};
2257 var kernel_name = kernelspec.name || this.default_kernel_name;
2257 var kernel_name = kernelspec.name || this.default_kernel_name;
2258
2258
2259 this.start_session(kernel_name);
2259 this.start_session(kernel_name);
2260 }
2260 }
2261 // load our checkpoint list
2261 // load our checkpoint list
2262 this.list_checkpoints();
2262 this.list_checkpoints();
2263
2263
2264 // load toolbar state
2264 // load toolbar state
2265 if (this.metadata.celltoolbar) {
2265 if (this.metadata.celltoolbar) {
2266 celltoolbar.CellToolbar.global_show();
2266 celltoolbar.CellToolbar.global_show();
2267 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2267 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2268 } else {
2268 } else {
2269 celltoolbar.CellToolbar.global_hide();
2269 celltoolbar.CellToolbar.global_hide();
2270 }
2270 }
2271
2271
2272 // now that we're fully loaded, it is safe to restore save functionality
2272 // now that we're fully loaded, it is safe to restore save functionality
2273 delete(this.save_notebook);
2273 delete(this.save_notebook);
2274 this.events.trigger('notebook_loaded.Notebook');
2274 this.events.trigger('notebook_loaded.Notebook');
2275 };
2275 };
2276
2276
2277 /**
2277 /**
2278 * Failure callback for loading a notebook from the server.
2278 * Failure callback for loading a notebook from the server.
2279 *
2279 *
2280 * @method load_notebook_error
2280 * @method load_notebook_error
2281 * @param {jqXHR} xhr jQuery Ajax object
2281 * @param {jqXHR} xhr jQuery Ajax object
2282 * @param {String} status Description of response status
2282 * @param {String} status Description of response status
2283 * @param {String} error HTTP error message
2283 * @param {String} error HTTP error message
2284 */
2284 */
2285 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2285 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2286 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2286 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2287 var msg;
2287 var msg;
2288 if (xhr.status === 400) {
2288 if (xhr.status === 400) {
2289 msg = error;
2289 msg = error;
2290 } else if (xhr.status === 500) {
2290 } else if (xhr.status === 500) {
2291 msg = "An unknown error occurred while loading this notebook. " +
2291 msg = "An unknown error occurred while loading this notebook. " +
2292 "This version can load notebook formats " +
2292 "This version can load notebook formats " +
2293 "v" + this.nbformat + " or earlier.";
2293 "v" + this.nbformat + " or earlier.";
2294 }
2294 }
2295 dialog.modal({
2295 dialog.modal({
2296 notebook: this,
2296 notebook: this,
2297 keyboard_manager: this.keyboard_manager,
2297 keyboard_manager: this.keyboard_manager,
2298 title: "Error loading notebook",
2298 title: "Error loading notebook",
2299 body : msg,
2299 body : msg,
2300 buttons : {
2300 buttons : {
2301 "OK": {}
2301 "OK": {}
2302 }
2302 }
2303 });
2303 });
2304 };
2304 };
2305
2305
2306 /********************* checkpoint-related *********************/
2306 /********************* checkpoint-related *********************/
2307
2307
2308 /**
2308 /**
2309 * Save the notebook then immediately create a checkpoint.
2309 * Save the notebook then immediately create a checkpoint.
2310 *
2310 *
2311 * @method save_checkpoint
2311 * @method save_checkpoint
2312 */
2312 */
2313 Notebook.prototype.save_checkpoint = function () {
2313 Notebook.prototype.save_checkpoint = function () {
2314 this._checkpoint_after_save = true;
2314 this._checkpoint_after_save = true;
2315 this.save_notebook();
2315 this.save_notebook();
2316 };
2316 };
2317
2317
2318 /**
2318 /**
2319 * Add a checkpoint for this notebook.
2319 * Add a checkpoint for this notebook.
2320 * for use as a callback from checkpoint creation.
2320 * for use as a callback from checkpoint creation.
2321 *
2321 *
2322 * @method add_checkpoint
2322 * @method add_checkpoint
2323 */
2323 */
2324 Notebook.prototype.add_checkpoint = function (checkpoint) {
2324 Notebook.prototype.add_checkpoint = function (checkpoint) {
2325 var found = false;
2325 var found = false;
2326 for (var i = 0; i < this.checkpoints.length; i++) {
2326 for (var i = 0; i < this.checkpoints.length; i++) {
2327 var existing = this.checkpoints[i];
2327 var existing = this.checkpoints[i];
2328 if (existing.id == checkpoint.id) {
2328 if (existing.id == checkpoint.id) {
2329 found = true;
2329 found = true;
2330 this.checkpoints[i] = checkpoint;
2330 this.checkpoints[i] = checkpoint;
2331 break;
2331 break;
2332 }
2332 }
2333 }
2333 }
2334 if (!found) {
2334 if (!found) {
2335 this.checkpoints.push(checkpoint);
2335 this.checkpoints.push(checkpoint);
2336 }
2336 }
2337 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2337 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2338 };
2338 };
2339
2339
2340 /**
2340 /**
2341 * List checkpoints for this notebook.
2341 * List checkpoints for this notebook.
2342 *
2342 *
2343 * @method list_checkpoints
2343 * @method list_checkpoints
2344 */
2344 */
2345 Notebook.prototype.list_checkpoints = function () {
2345 Notebook.prototype.list_checkpoints = function () {
2346 var url = utils.url_join_encode(
2346 var url = utils.url_join_encode(
2347 this.base_url,
2347 this.base_url,
2348 'api/notebooks',
2348 'api/contents',
2349 this.notebook_path,
2349 this.notebook_path,
2350 this.notebook_name,
2350 this.notebook_name,
2351 'checkpoints'
2351 'checkpoints'
2352 );
2352 );
2353 $.get(url).done(
2353 $.get(url).done(
2354 $.proxy(this.list_checkpoints_success, this)
2354 $.proxy(this.list_checkpoints_success, this)
2355 ).fail(
2355 ).fail(
2356 $.proxy(this.list_checkpoints_error, this)
2356 $.proxy(this.list_checkpoints_error, this)
2357 );
2357 );
2358 };
2358 };
2359
2359
2360 /**
2360 /**
2361 * Success callback for listing checkpoints.
2361 * Success callback for listing checkpoints.
2362 *
2362 *
2363 * @method list_checkpoint_success
2363 * @method list_checkpoint_success
2364 * @param {Object} data JSON representation of a checkpoint
2364 * @param {Object} data JSON representation of a checkpoint
2365 * @param {String} status Description of response status
2365 * @param {String} status Description of response status
2366 * @param {jqXHR} xhr jQuery Ajax object
2366 * @param {jqXHR} xhr jQuery Ajax object
2367 */
2367 */
2368 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2368 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2369 data = $.parseJSON(data);
2369 data = $.parseJSON(data);
2370 this.checkpoints = data;
2370 this.checkpoints = data;
2371 if (data.length) {
2371 if (data.length) {
2372 this.last_checkpoint = data[data.length - 1];
2372 this.last_checkpoint = data[data.length - 1];
2373 } else {
2373 } else {
2374 this.last_checkpoint = null;
2374 this.last_checkpoint = null;
2375 }
2375 }
2376 this.events.trigger('checkpoints_listed.Notebook', [data]);
2376 this.events.trigger('checkpoints_listed.Notebook', [data]);
2377 };
2377 };
2378
2378
2379 /**
2379 /**
2380 * Failure callback for listing a checkpoint.
2380 * Failure callback for listing a checkpoint.
2381 *
2381 *
2382 * @method list_checkpoint_error
2382 * @method list_checkpoint_error
2383 * @param {jqXHR} xhr jQuery Ajax object
2383 * @param {jqXHR} xhr jQuery Ajax object
2384 * @param {String} status Description of response status
2384 * @param {String} status Description of response status
2385 * @param {String} error_msg HTTP error message
2385 * @param {String} error_msg HTTP error message
2386 */
2386 */
2387 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2387 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2388 this.events.trigger('list_checkpoints_failed.Notebook');
2388 this.events.trigger('list_checkpoints_failed.Notebook');
2389 };
2389 };
2390
2390
2391 /**
2391 /**
2392 * Create a checkpoint of this notebook on the server from the most recent save.
2392 * Create a checkpoint of this notebook on the server from the most recent save.
2393 *
2393 *
2394 * @method create_checkpoint
2394 * @method create_checkpoint
2395 */
2395 */
2396 Notebook.prototype.create_checkpoint = function () {
2396 Notebook.prototype.create_checkpoint = function () {
2397 var url = utils.url_join_encode(
2397 var url = utils.url_join_encode(
2398 this.base_url,
2398 this.base_url,
2399 'api/notebooks',
2399 'api/contents',
2400 this.notebook_path,
2400 this.notebook_path,
2401 this.notebook_name,
2401 this.notebook_name,
2402 'checkpoints'
2402 'checkpoints'
2403 );
2403 );
2404 $.post(url).done(
2404 $.post(url).done(
2405 $.proxy(this.create_checkpoint_success, this)
2405 $.proxy(this.create_checkpoint_success, this)
2406 ).fail(
2406 ).fail(
2407 $.proxy(this.create_checkpoint_error, this)
2407 $.proxy(this.create_checkpoint_error, this)
2408 );
2408 );
2409 };
2409 };
2410
2410
2411 /**
2411 /**
2412 * Success callback for creating a checkpoint.
2412 * Success callback for creating a checkpoint.
2413 *
2413 *
2414 * @method create_checkpoint_success
2414 * @method create_checkpoint_success
2415 * @param {Object} data JSON representation of a checkpoint
2415 * @param {Object} data JSON representation of a checkpoint
2416 * @param {String} status Description of response status
2416 * @param {String} status Description of response status
2417 * @param {jqXHR} xhr jQuery Ajax object
2417 * @param {jqXHR} xhr jQuery Ajax object
2418 */
2418 */
2419 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2419 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2420 data = $.parseJSON(data);
2420 data = $.parseJSON(data);
2421 this.add_checkpoint(data);
2421 this.add_checkpoint(data);
2422 this.events.trigger('checkpoint_created.Notebook', data);
2422 this.events.trigger('checkpoint_created.Notebook', data);
2423 };
2423 };
2424
2424
2425 /**
2425 /**
2426 * Failure callback for creating a checkpoint.
2426 * Failure callback for creating a checkpoint.
2427 *
2427 *
2428 * @method create_checkpoint_error
2428 * @method create_checkpoint_error
2429 * @param {jqXHR} xhr jQuery Ajax object
2429 * @param {jqXHR} xhr jQuery Ajax object
2430 * @param {String} status Description of response status
2430 * @param {String} status Description of response status
2431 * @param {String} error_msg HTTP error message
2431 * @param {String} error_msg HTTP error message
2432 */
2432 */
2433 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2433 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2434 this.events.trigger('checkpoint_failed.Notebook');
2434 this.events.trigger('checkpoint_failed.Notebook');
2435 };
2435 };
2436
2436
2437 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2437 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2438 var that = this;
2438 var that = this;
2439 checkpoint = checkpoint || this.last_checkpoint;
2439 checkpoint = checkpoint || this.last_checkpoint;
2440 if ( ! checkpoint ) {
2440 if ( ! checkpoint ) {
2441 console.log("restore dialog, but no checkpoint to restore to!");
2441 console.log("restore dialog, but no checkpoint to restore to!");
2442 return;
2442 return;
2443 }
2443 }
2444 var body = $('<div/>').append(
2444 var body = $('<div/>').append(
2445 $('<p/>').addClass("p-space").text(
2445 $('<p/>').addClass("p-space").text(
2446 "Are you sure you want to revert the notebook to " +
2446 "Are you sure you want to revert the notebook to " +
2447 "the latest checkpoint?"
2447 "the latest checkpoint?"
2448 ).append(
2448 ).append(
2449 $("<strong/>").text(
2449 $("<strong/>").text(
2450 " This cannot be undone."
2450 " This cannot be undone."
2451 )
2451 )
2452 )
2452 )
2453 ).append(
2453 ).append(
2454 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2454 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2455 ).append(
2455 ).append(
2456 $('<p/>').addClass("p-space").text(
2456 $('<p/>').addClass("p-space").text(
2457 Date(checkpoint.last_modified)
2457 Date(checkpoint.last_modified)
2458 ).css("text-align", "center")
2458 ).css("text-align", "center")
2459 );
2459 );
2460
2460
2461 dialog.modal({
2461 dialog.modal({
2462 notebook: this,
2462 notebook: this,
2463 keyboard_manager: this.keyboard_manager,
2463 keyboard_manager: this.keyboard_manager,
2464 title : "Revert notebook to checkpoint",
2464 title : "Revert notebook to checkpoint",
2465 body : body,
2465 body : body,
2466 buttons : {
2466 buttons : {
2467 Revert : {
2467 Revert : {
2468 class : "btn-danger",
2468 class : "btn-danger",
2469 click : function () {
2469 click : function () {
2470 that.restore_checkpoint(checkpoint.id);
2470 that.restore_checkpoint(checkpoint.id);
2471 }
2471 }
2472 },
2472 },
2473 Cancel : {}
2473 Cancel : {}
2474 }
2474 }
2475 });
2475 });
2476 };
2476 };
2477
2477
2478 /**
2478 /**
2479 * Restore the notebook to a checkpoint state.
2479 * Restore the notebook to a checkpoint state.
2480 *
2480 *
2481 * @method restore_checkpoint
2481 * @method restore_checkpoint
2482 * @param {String} checkpoint ID
2482 * @param {String} checkpoint ID
2483 */
2483 */
2484 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2484 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2485 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2485 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2486 var url = utils.url_join_encode(
2486 var url = utils.url_join_encode(
2487 this.base_url,
2487 this.base_url,
2488 'api/notebooks',
2488 'api/contents',
2489 this.notebook_path,
2489 this.notebook_path,
2490 this.notebook_name,
2490 this.notebook_name,
2491 'checkpoints',
2491 'checkpoints',
2492 checkpoint
2492 checkpoint
2493 );
2493 );
2494 $.post(url).done(
2494 $.post(url).done(
2495 $.proxy(this.restore_checkpoint_success, this)
2495 $.proxy(this.restore_checkpoint_success, this)
2496 ).fail(
2496 ).fail(
2497 $.proxy(this.restore_checkpoint_error, this)
2497 $.proxy(this.restore_checkpoint_error, this)
2498 );
2498 );
2499 };
2499 };
2500
2500
2501 /**
2501 /**
2502 * Success callback for restoring a notebook to a checkpoint.
2502 * Success callback for restoring a notebook to a checkpoint.
2503 *
2503 *
2504 * @method restore_checkpoint_success
2504 * @method restore_checkpoint_success
2505 * @param {Object} data (ignored, should be empty)
2505 * @param {Object} data (ignored, should be empty)
2506 * @param {String} status Description of response status
2506 * @param {String} status Description of response status
2507 * @param {jqXHR} xhr jQuery Ajax object
2507 * @param {jqXHR} xhr jQuery Ajax object
2508 */
2508 */
2509 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2509 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2510 this.events.trigger('checkpoint_restored.Notebook');
2510 this.events.trigger('checkpoint_restored.Notebook');
2511 this.load_notebook(this.notebook_name, this.notebook_path);
2511 this.load_notebook(this.notebook_name, this.notebook_path);
2512 };
2512 };
2513
2513
2514 /**
2514 /**
2515 * Failure callback for restoring a notebook to a checkpoint.
2515 * Failure callback for restoring a notebook to a checkpoint.
2516 *
2516 *
2517 * @method restore_checkpoint_error
2517 * @method restore_checkpoint_error
2518 * @param {jqXHR} xhr jQuery Ajax object
2518 * @param {jqXHR} xhr jQuery Ajax object
2519 * @param {String} status Description of response status
2519 * @param {String} status Description of response status
2520 * @param {String} error_msg HTTP error message
2520 * @param {String} error_msg HTTP error message
2521 */
2521 */
2522 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2522 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2523 this.events.trigger('checkpoint_restore_failed.Notebook');
2523 this.events.trigger('checkpoint_restore_failed.Notebook');
2524 };
2524 };
2525
2525
2526 /**
2526 /**
2527 * Delete a notebook checkpoint.
2527 * Delete a notebook checkpoint.
2528 *
2528 *
2529 * @method delete_checkpoint
2529 * @method delete_checkpoint
2530 * @param {String} checkpoint ID
2530 * @param {String} checkpoint ID
2531 */
2531 */
2532 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2532 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2533 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2533 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2534 var url = utils.url_join_encode(
2534 var url = utils.url_join_encode(
2535 this.base_url,
2535 this.base_url,
2536 'api/notebooks',
2536 'api/contents',
2537 this.notebook_path,
2537 this.notebook_path,
2538 this.notebook_name,
2538 this.notebook_name,
2539 'checkpoints',
2539 'checkpoints',
2540 checkpoint
2540 checkpoint
2541 );
2541 );
2542 $.ajax(url, {
2542 $.ajax(url, {
2543 type: 'DELETE',
2543 type: 'DELETE',
2544 success: $.proxy(this.delete_checkpoint_success, this),
2544 success: $.proxy(this.delete_checkpoint_success, this),
2545 error: $.proxy(this.delete_checkpoint_error, this)
2545 error: $.proxy(this.delete_checkpoint_error, this)
2546 });
2546 });
2547 };
2547 };
2548
2548
2549 /**
2549 /**
2550 * Success callback for deleting a notebook checkpoint
2550 * Success callback for deleting a notebook checkpoint
2551 *
2551 *
2552 * @method delete_checkpoint_success
2552 * @method delete_checkpoint_success
2553 * @param {Object} data (ignored, should be empty)
2553 * @param {Object} data (ignored, should be empty)
2554 * @param {String} status Description of response status
2554 * @param {String} status Description of response status
2555 * @param {jqXHR} xhr jQuery Ajax object
2555 * @param {jqXHR} xhr jQuery Ajax object
2556 */
2556 */
2557 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2557 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2558 this.events.trigger('checkpoint_deleted.Notebook', data);
2558 this.events.trigger('checkpoint_deleted.Notebook', data);
2559 this.load_notebook(this.notebook_name, this.notebook_path);
2559 this.load_notebook(this.notebook_name, this.notebook_path);
2560 };
2560 };
2561
2561
2562 /**
2562 /**
2563 * Failure callback for deleting a notebook checkpoint.
2563 * Failure callback for deleting a notebook checkpoint.
2564 *
2564 *
2565 * @method delete_checkpoint_error
2565 * @method delete_checkpoint_error
2566 * @param {jqXHR} xhr jQuery Ajax object
2566 * @param {jqXHR} xhr jQuery Ajax object
2567 * @param {String} status Description of response status
2567 * @param {String} status Description of response status
2568 * @param {String} error_msg HTTP error message
2568 * @param {String} error_msg HTTP error message
2569 */
2569 */
2570 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2570 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2571 this.events.trigger('checkpoint_delete_failed.Notebook');
2571 this.events.trigger('checkpoint_delete_failed.Notebook');
2572 };
2572 };
2573
2573
2574
2574
2575 // For backwards compatability.
2575 // For backwards compatability.
2576 IPython.Notebook = Notebook;
2576 IPython.Notebook = Notebook;
2577
2577
2578 return {'Notebook': Notebook};
2578 return {'Notebook': Notebook};
2579 });
2579 });
@@ -1,448 +1,448 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 'base/js/dialog',
8 'base/js/dialog',
9 ], function(IPython, $, utils, dialog) {
9 ], function(IPython, $, utils, dialog) {
10 "use strict";
10 "use strict";
11
11
12 var NotebookList = function (selector, options) {
12 var NotebookList = function (selector, options) {
13 // Constructor
13 // Constructor
14 //
14 //
15 // Parameters:
15 // Parameters:
16 // selector: string
16 // selector: string
17 // options: dictionary
17 // options: dictionary
18 // Dictionary of keyword arguments.
18 // Dictionary of keyword arguments.
19 // session_list: SessionList instance
19 // session_list: SessionList instance
20 // element_name: string
20 // element_name: string
21 // base_url: string
21 // base_url: string
22 // notebook_path: string
22 // notebook_path: string
23 var that = this;
23 var that = this;
24 this.session_list = options.session_list;
24 this.session_list = options.session_list;
25 // allow code re-use by just changing element_name in kernellist.js
25 // allow code re-use by just changing element_name in kernellist.js
26 this.element_name = options.element_name || 'notebook';
26 this.element_name = options.element_name || 'notebook';
27 this.selector = selector;
27 this.selector = selector;
28 if (this.selector !== undefined) {
28 if (this.selector !== undefined) {
29 this.element = $(selector);
29 this.element = $(selector);
30 this.style();
30 this.style();
31 this.bind_events();
31 this.bind_events();
32 }
32 }
33 this.notebooks_list = [];
33 this.notebooks_list = [];
34 this.sessions = {};
34 this.sessions = {};
35 this.base_url = options.base_url || utils.get_body_data("baseUrl");
35 this.base_url = options.base_url || utils.get_body_data("baseUrl");
36 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
36 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
37 if (this.session_list && this.session_list.events) {
37 if (this.session_list && this.session_list.events) {
38 this.session_list.events.on('sessions_loaded.Dashboard',
38 this.session_list.events.on('sessions_loaded.Dashboard',
39 function(e, d) { that.sessions_loaded(d); });
39 function(e, d) { that.sessions_loaded(d); });
40 }
40 }
41 };
41 };
42
42
43 NotebookList.prototype.style = function () {
43 NotebookList.prototype.style = function () {
44 var prefix = '#' + this.element_name;
44 var prefix = '#' + this.element_name;
45 $(prefix + '_toolbar').addClass('list_toolbar');
45 $(prefix + '_toolbar').addClass('list_toolbar');
46 $(prefix + '_list_info').addClass('toolbar_info');
46 $(prefix + '_list_info').addClass('toolbar_info');
47 $(prefix + '_buttons').addClass('toolbar_buttons');
47 $(prefix + '_buttons').addClass('toolbar_buttons');
48 $(prefix + '_list_header').addClass('list_header');
48 $(prefix + '_list_header').addClass('list_header');
49 this.element.addClass("list_container");
49 this.element.addClass("list_container");
50 };
50 };
51
51
52
52
53 NotebookList.prototype.bind_events = function () {
53 NotebookList.prototype.bind_events = function () {
54 var that = this;
54 var that = this;
55 $('#refresh_' + this.element_name + '_list').click(function () {
55 $('#refresh_' + this.element_name + '_list').click(function () {
56 that.load_sessions();
56 that.load_sessions();
57 });
57 });
58 this.element.bind('dragover', function () {
58 this.element.bind('dragover', function () {
59 return false;
59 return false;
60 });
60 });
61 this.element.bind('drop', function(event){
61 this.element.bind('drop', function(event){
62 that.handleFilesUpload(event,'drop');
62 that.handleFilesUpload(event,'drop');
63 return false;
63 return false;
64 });
64 });
65 };
65 };
66
66
67 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
67 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
68 var that = this;
68 var that = this;
69 var files;
69 var files;
70 if(dropOrForm =='drop'){
70 if(dropOrForm =='drop'){
71 files = event.originalEvent.dataTransfer.files;
71 files = event.originalEvent.dataTransfer.files;
72 } else
72 } else
73 {
73 {
74 files = event.originalEvent.target.files;
74 files = event.originalEvent.target.files;
75 }
75 }
76 for (var i = 0; i < files.length; i++) {
76 for (var i = 0; i < files.length; i++) {
77 var f = files[i];
77 var f = files[i];
78 var reader = new FileReader();
78 var reader = new FileReader();
79 reader.readAsText(f);
79 reader.readAsText(f);
80 var name_and_ext = utils.splitext(f.name);
80 var name_and_ext = utils.splitext(f.name);
81 var file_ext = name_and_ext[1];
81 var file_ext = name_and_ext[1];
82 if (file_ext === '.ipynb') {
82 if (file_ext === '.ipynb') {
83 var item = that.new_notebook_item(0);
83 var item = that.new_notebook_item(0);
84 item.addClass('new-file');
84 item.addClass('new-file');
85 that.add_name_input(f.name, item);
85 that.add_name_input(f.name, item);
86 // Store the notebook item in the reader so we can use it later
86 // Store the notebook item in the reader so we can use it later
87 // to know which item it belongs to.
87 // to know which item it belongs to.
88 $(reader).data('item', item);
88 $(reader).data('item', item);
89 reader.onload = function (event) {
89 reader.onload = function (event) {
90 var nbitem = $(event.target).data('item');
90 var nbitem = $(event.target).data('item');
91 that.add_notebook_data(event.target.result, nbitem);
91 that.add_notebook_data(event.target.result, nbitem);
92 that.add_upload_button(nbitem);
92 that.add_upload_button(nbitem);
93 };
93 };
94 } else {
94 } else {
95 var dialog_body = 'Uploaded notebooks must be .ipynb files';
95 var dialog_body = 'Uploaded notebooks must be .ipynb files';
96 dialog.modal({
96 dialog.modal({
97 title : 'Invalid file type',
97 title : 'Invalid file type',
98 body : dialog_body,
98 body : dialog_body,
99 buttons : {'OK' : {'class' : 'btn-primary'}}
99 buttons : {'OK' : {'class' : 'btn-primary'}}
100 });
100 });
101 }
101 }
102 }
102 }
103 // Replace the file input form wth a clone of itself. This is required to
103 // Replace the file input form wth a clone of itself. This is required to
104 // reset the form. Otherwise, if you upload a file, delete it and try to
104 // reset the form. Otherwise, if you upload a file, delete it and try to
105 // upload it again, the changed event won't fire.
105 // upload it again, the changed event won't fire.
106 var form = $('input.fileinput');
106 var form = $('input.fileinput');
107 form.replaceWith(form.clone(true));
107 form.replaceWith(form.clone(true));
108 return false;
108 return false;
109 };
109 };
110
110
111 NotebookList.prototype.clear_list = function (remove_uploads) {
111 NotebookList.prototype.clear_list = function (remove_uploads) {
112 // Clears the navigation tree.
112 // Clears the navigation tree.
113 //
113 //
114 // Parameters
114 // Parameters
115 // remove_uploads: bool=False
115 // remove_uploads: bool=False
116 // Should upload prompts also be removed from the tree.
116 // Should upload prompts also be removed from the tree.
117 if (remove_uploads) {
117 if (remove_uploads) {
118 this.element.children('.list_item').remove();
118 this.element.children('.list_item').remove();
119 } else {
119 } else {
120 this.element.children('.list_item:not(.new-file)').remove();
120 this.element.children('.list_item:not(.new-file)').remove();
121 }
121 }
122 };
122 };
123
123
124 NotebookList.prototype.load_sessions = function(){
124 NotebookList.prototype.load_sessions = function(){
125 this.session_list.load_sessions();
125 this.session_list.load_sessions();
126 };
126 };
127
127
128
128
129 NotebookList.prototype.sessions_loaded = function(data){
129 NotebookList.prototype.sessions_loaded = function(data){
130 this.sessions = data;
130 this.sessions = data;
131 this.load_list();
131 this.load_list();
132 };
132 };
133
133
134 NotebookList.prototype.load_list = function () {
134 NotebookList.prototype.load_list = function () {
135 var that = this;
135 var that = this;
136 var settings = {
136 var settings = {
137 processData : false,
137 processData : false,
138 cache : false,
138 cache : false,
139 type : "GET",
139 type : "GET",
140 dataType : "json",
140 dataType : "json",
141 success : $.proxy(this.list_loaded, this),
141 success : $.proxy(this.list_loaded, this),
142 error : $.proxy( function(xhr, status, error){
142 error : $.proxy( function(xhr, status, error){
143 utils.log_ajax_error(xhr, status, error);
143 utils.log_ajax_error(xhr, status, error);
144 that.list_loaded([], null, null, {msg:"Error connecting to server."});
144 that.list_loaded([], null, null, {msg:"Error connecting to server."});
145 },this)
145 },this)
146 };
146 };
147
147
148 var url = utils.url_join_encode(
148 var url = utils.url_join_encode(
149 this.base_url,
149 this.base_url,
150 'api',
150 'api',
151 'notebooks',
151 'contents',
152 this.notebook_path
152 this.notebook_path
153 );
153 );
154 $.ajax(url, settings);
154 $.ajax(url, settings);
155 };
155 };
156
156
157
157
158 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
158 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
159 var message = 'Notebook list empty.';
159 var message = 'Notebook list empty.';
160 if (param !== undefined && param.msg) {
160 if (param !== undefined && param.msg) {
161 message = param.msg;
161 message = param.msg;
162 }
162 }
163 var item = null;
163 var item = null;
164 var len = data.length;
164 var len = data.length;
165 this.clear_list();
165 this.clear_list();
166 if (len === 0) {
166 if (len === 0) {
167 item = this.new_notebook_item(0);
167 item = this.new_notebook_item(0);
168 var span12 = item.children().first();
168 var span12 = item.children().first();
169 span12.empty();
169 span12.empty();
170 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
170 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
171 }
171 }
172 var path = this.notebook_path;
172 var path = this.notebook_path;
173 var offset = 0;
173 var offset = 0;
174 if (path !== '') {
174 if (path !== '') {
175 item = this.new_notebook_item(0);
175 item = this.new_notebook_item(0);
176 this.add_dir(path, '..', item);
176 this.add_dir(path, '..', item);
177 offset = 1;
177 offset = 1;
178 }
178 }
179 for (var i=0; i<len; i++) {
179 for (var i=0; i<len; i++) {
180 if (data[i].type === 'directory') {
180 if (data[i].type === 'directory') {
181 var name = data[i].name;
181 var name = data[i].name;
182 item = this.new_notebook_item(i+offset);
182 item = this.new_notebook_item(i+offset);
183 this.add_dir(path, name, item);
183 this.add_dir(path, name, item);
184 } else {
184 } else {
185 var name = data[i].name;
185 var name = data[i].name;
186 item = this.new_notebook_item(i+offset);
186 item = this.new_notebook_item(i+offset);
187 this.add_link(path, name, item);
187 this.add_link(path, name, item);
188 name = utils.url_path_join(path, name);
188 name = utils.url_path_join(path, name);
189 if(this.sessions[name] === undefined){
189 if(this.sessions[name] === undefined){
190 this.add_delete_button(item);
190 this.add_delete_button(item);
191 } else {
191 } else {
192 this.add_shutdown_button(item,this.sessions[name]);
192 this.add_shutdown_button(item,this.sessions[name]);
193 }
193 }
194 }
194 }
195 }
195 }
196 };
196 };
197
197
198
198
199 NotebookList.prototype.new_notebook_item = function (index) {
199 NotebookList.prototype.new_notebook_item = function (index) {
200 var item = $('<div/>').addClass("list_item").addClass("row");
200 var item = $('<div/>').addClass("list_item").addClass("row");
201 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
201 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
202 // item.css('border-top-style','none');
202 // item.css('border-top-style','none');
203 item.append($("<div/>").addClass("col-md-12").append(
203 item.append($("<div/>").addClass("col-md-12").append(
204 $('<i/>').addClass('item_icon')
204 $('<i/>').addClass('item_icon')
205 ).append(
205 ).append(
206 $("<a/>").addClass("item_link").append(
206 $("<a/>").addClass("item_link").append(
207 $("<span/>").addClass("item_name")
207 $("<span/>").addClass("item_name")
208 )
208 )
209 ).append(
209 ).append(
210 $('<div/>').addClass("item_buttons btn-group pull-right")
210 $('<div/>').addClass("item_buttons btn-group pull-right")
211 ));
211 ));
212
212
213 if (index === -1) {
213 if (index === -1) {
214 this.element.append(item);
214 this.element.append(item);
215 } else {
215 } else {
216 this.element.children().eq(index).after(item);
216 this.element.children().eq(index).after(item);
217 }
217 }
218 return item;
218 return item;
219 };
219 };
220
220
221
221
222 NotebookList.prototype.add_dir = function (path, name, item) {
222 NotebookList.prototype.add_dir = function (path, name, item) {
223 item.data('name', name);
223 item.data('name', name);
224 item.data('path', path);
224 item.data('path', path);
225 item.find(".item_name").text(name);
225 item.find(".item_name").text(name);
226 item.find(".item_icon").addClass('folder_icon').addClass('icon-fixed-width');
226 item.find(".item_icon").addClass('folder_icon').addClass('icon-fixed-width');
227 item.find("a.item_link")
227 item.find("a.item_link")
228 .attr('href',
228 .attr('href',
229 utils.url_join_encode(
229 utils.url_join_encode(
230 this.base_url,
230 this.base_url,
231 "tree",
231 "tree",
232 path,
232 path,
233 name
233 name
234 )
234 )
235 );
235 );
236 };
236 };
237
237
238
238
239 NotebookList.prototype.add_link = function (path, nbname, item) {
239 NotebookList.prototype.add_link = function (path, nbname, item) {
240 item.data('nbname', nbname);
240 item.data('nbname', nbname);
241 item.data('path', path);
241 item.data('path', path);
242 item.find(".item_name").text(nbname);
242 item.find(".item_name").text(nbname);
243 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
243 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
244 item.find("a.item_link")
244 item.find("a.item_link")
245 .attr('href',
245 .attr('href',
246 utils.url_join_encode(
246 utils.url_join_encode(
247 this.base_url,
247 this.base_url,
248 "notebooks",
248 "notebooks",
249 path,
249 path,
250 nbname
250 nbname
251 )
251 )
252 ).attr('target','_blank');
252 ).attr('target','_blank');
253 };
253 };
254
254
255
255
256 NotebookList.prototype.add_name_input = function (nbname, item) {
256 NotebookList.prototype.add_name_input = function (nbname, item) {
257 item.data('nbname', nbname);
257 item.data('nbname', nbname);
258 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
258 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
259 item.find(".item_name").empty().append(
259 item.find(".item_name").empty().append(
260 $('<input/>')
260 $('<input/>')
261 .addClass("nbname_input")
261 .addClass("nbname_input")
262 .attr('value', utils.splitext(nbname)[0])
262 .attr('value', utils.splitext(nbname)[0])
263 .attr('size', '30')
263 .attr('size', '30')
264 .attr('type', 'text')
264 .attr('type', 'text')
265 );
265 );
266 };
266 };
267
267
268
268
269 NotebookList.prototype.add_notebook_data = function (data, item) {
269 NotebookList.prototype.add_notebook_data = function (data, item) {
270 item.data('nbdata', data);
270 item.data('nbdata', data);
271 };
271 };
272
272
273
273
274 NotebookList.prototype.add_shutdown_button = function (item, session) {
274 NotebookList.prototype.add_shutdown_button = function (item, session) {
275 var that = this;
275 var that = this;
276 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
276 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
277 click(function (e) {
277 click(function (e) {
278 var settings = {
278 var settings = {
279 processData : false,
279 processData : false,
280 cache : false,
280 cache : false,
281 type : "DELETE",
281 type : "DELETE",
282 dataType : "json",
282 dataType : "json",
283 success : function () {
283 success : function () {
284 that.load_sessions();
284 that.load_sessions();
285 },
285 },
286 error : utils.log_ajax_error,
286 error : utils.log_ajax_error,
287 };
287 };
288 var url = utils.url_join_encode(
288 var url = utils.url_join_encode(
289 that.base_url,
289 that.base_url,
290 'api/sessions',
290 'api/sessions',
291 session
291 session
292 );
292 );
293 $.ajax(url, settings);
293 $.ajax(url, settings);
294 return false;
294 return false;
295 });
295 });
296 // var new_buttons = item.find('a'); // shutdown_button;
296 // var new_buttons = item.find('a'); // shutdown_button;
297 item.find(".item_buttons").text("").append(shutdown_button);
297 item.find(".item_buttons").text("").append(shutdown_button);
298 };
298 };
299
299
300 NotebookList.prototype.add_delete_button = function (item) {
300 NotebookList.prototype.add_delete_button = function (item) {
301 var new_buttons = $('<span/>').addClass("btn-group pull-right");
301 var new_buttons = $('<span/>').addClass("btn-group pull-right");
302 var notebooklist = this;
302 var notebooklist = this;
303 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
303 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
304 click(function (e) {
304 click(function (e) {
305 // $(this) is the button that was clicked.
305 // $(this) is the button that was clicked.
306 var that = $(this);
306 var that = $(this);
307 // We use the nbname and notebook_id from the parent notebook_item element's
307 // We use the nbname and notebook_id from the parent notebook_item element's
308 // data because the outer scopes values change as we iterate through the loop.
308 // data because the outer scopes values change as we iterate through the loop.
309 var parent_item = that.parents('div.list_item');
309 var parent_item = that.parents('div.list_item');
310 var nbname = parent_item.data('nbname');
310 var nbname = parent_item.data('nbname');
311 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
311 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
312 dialog.modal({
312 dialog.modal({
313 title : "Delete notebook",
313 title : "Delete notebook",
314 body : message,
314 body : message,
315 buttons : {
315 buttons : {
316 Delete : {
316 Delete : {
317 class: "btn-danger",
317 class: "btn-danger",
318 click: function() {
318 click: function() {
319 var settings = {
319 var settings = {
320 processData : false,
320 processData : false,
321 cache : false,
321 cache : false,
322 type : "DELETE",
322 type : "DELETE",
323 dataType : "json",
323 dataType : "json",
324 success : function (data, status, xhr) {
324 success : function (data, status, xhr) {
325 parent_item.remove();
325 parent_item.remove();
326 },
326 },
327 error : utils.log_ajax_error,
327 error : utils.log_ajax_error,
328 };
328 };
329 var url = utils.url_join_encode(
329 var url = utils.url_join_encode(
330 notebooklist.base_url,
330 notebooklist.base_url,
331 'api/notebooks',
331 'api/contents',
332 notebooklist.notebook_path,
332 notebooklist.notebook_path,
333 nbname
333 nbname
334 );
334 );
335 $.ajax(url, settings);
335 $.ajax(url, settings);
336 }
336 }
337 },
337 },
338 Cancel : {}
338 Cancel : {}
339 }
339 }
340 });
340 });
341 return false;
341 return false;
342 });
342 });
343 item.find(".item_buttons").text("").append(delete_button);
343 item.find(".item_buttons").text("").append(delete_button);
344 };
344 };
345
345
346
346
347 NotebookList.prototype.add_upload_button = function (item) {
347 NotebookList.prototype.add_upload_button = function (item) {
348 var that = this;
348 var that = this;
349 var upload_button = $('<button/>').text("Upload")
349 var upload_button = $('<button/>').text("Upload")
350 .addClass('btn btn-primary btn-xs upload_button')
350 .addClass('btn btn-primary btn-xs upload_button')
351 .click(function (e) {
351 .click(function (e) {
352 var nbname = item.find('.item_name > input').val();
352 var nbname = item.find('.item_name > input').val();
353 if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
353 if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
354 nbname = nbname + ".ipynb";
354 nbname = nbname + ".ipynb";
355 }
355 }
356 var path = that.notebook_path;
356 var path = that.notebook_path;
357 var nbdata = item.data('nbdata');
357 var nbdata = item.data('nbdata');
358 var content_type = 'application/json';
358 var content_type = 'application/json';
359 var model = {
359 var model = {
360 content : JSON.parse(nbdata),
360 content : JSON.parse(nbdata),
361 };
361 };
362 var settings = {
362 var settings = {
363 processData : false,
363 processData : false,
364 cache : false,
364 cache : false,
365 type : 'PUT',
365 type : 'PUT',
366 dataType : 'json',
366 dataType : 'json',
367 data : JSON.stringify(model),
367 data : JSON.stringify(model),
368 headers : {'Content-Type': content_type},
368 headers : {'Content-Type': content_type},
369 success : function (data, status, xhr) {
369 success : function (data, status, xhr) {
370 that.add_link(path, nbname, item);
370 that.add_link(path, nbname, item);
371 that.add_delete_button(item);
371 that.add_delete_button(item);
372 },
372 },
373 error : utils.log_ajax_error,
373 error : utils.log_ajax_error,
374 };
374 };
375
375
376 var url = utils.url_join_encode(
376 var url = utils.url_join_encode(
377 that.base_url,
377 that.base_url,
378 'api/notebooks',
378 'api/contents',
379 that.notebook_path,
379 that.notebook_path,
380 nbname
380 nbname
381 );
381 );
382 $.ajax(url, settings);
382 $.ajax(url, settings);
383 return false;
383 return false;
384 });
384 });
385 var cancel_button = $('<button/>').text("Cancel")
385 var cancel_button = $('<button/>').text("Cancel")
386 .addClass("btn btn-default btn-xs")
386 .addClass("btn btn-default btn-xs")
387 .click(function (e) {
387 .click(function (e) {
388 console.log('cancel click');
388 console.log('cancel click');
389 item.remove();
389 item.remove();
390 return false;
390 return false;
391 });
391 });
392 item.find(".item_buttons").empty()
392 item.find(".item_buttons").empty()
393 .append(upload_button)
393 .append(upload_button)
394 .append(cancel_button);
394 .append(cancel_button);
395 };
395 };
396
396
397
397
398 NotebookList.prototype.new_notebook = function(){
398 NotebookList.prototype.new_notebook = function(){
399 var path = this.notebook_path;
399 var path = this.notebook_path;
400 var base_url = this.base_url;
400 var base_url = this.base_url;
401 var settings = {
401 var settings = {
402 processData : false,
402 processData : false,
403 cache : false,
403 cache : false,
404 type : "POST",
404 type : "POST",
405 dataType : "json",
405 dataType : "json",
406 async : false,
406 async : false,
407 success : function (data, status, xhr) {
407 success : function (data, status, xhr) {
408 var notebook_name = data.name;
408 var notebook_name = data.name;
409 window.open(
409 window.open(
410 utils.url_join_encode(
410 utils.url_join_encode(
411 base_url,
411 base_url,
412 'notebooks',
412 'notebooks',
413 path,
413 path,
414 notebook_name),
414 notebook_name),
415 '_blank'
415 '_blank'
416 );
416 );
417 },
417 },
418 error : $.proxy(this.new_notebook_failed, this),
418 error : $.proxy(this.new_notebook_failed, this),
419 };
419 };
420 var url = utils.url_join_encode(
420 var url = utils.url_join_encode(
421 base_url,
421 base_url,
422 'api/notebooks',
422 'api/contents',
423 path
423 path
424 );
424 );
425 $.ajax(url, settings);
425 $.ajax(url, settings);
426 };
426 };
427
427
428
428
429 NotebookList.prototype.new_notebook_failed = function (xhr, status, error) {
429 NotebookList.prototype.new_notebook_failed = function (xhr, status, error) {
430 utils.log_ajax_error(xhr, status, error);
430 utils.log_ajax_error(xhr, status, error);
431 var msg;
431 var msg;
432 if (xhr.responseJSON && xhr.responseJSON.message) {
432 if (xhr.responseJSON && xhr.responseJSON.message) {
433 msg = xhr.responseJSON.message;
433 msg = xhr.responseJSON.message;
434 } else {
434 } else {
435 msg = xhr.statusText;
435 msg = xhr.statusText;
436 }
436 }
437 dialog.modal({
437 dialog.modal({
438 title : 'Creating Notebook Failed',
438 title : 'Creating Notebook Failed',
439 body : "The error was: " + msg,
439 body : "The error was: " + msg,
440 buttons : {'OK' : {'class' : 'btn-primary'}}
440 buttons : {'OK' : {'class' : 'btn-primary'}}
441 });
441 });
442 };
442 };
443
443
444 // Backwards compatability.
444 // Backwards compatability.
445 IPython.NotebookList = NotebookList;
445 IPython.NotebookList = NotebookList;
446
446
447 return {'NotebookList': NotebookList};
447 return {'NotebookList': NotebookList};
448 });
448 });
@@ -1,102 +1,102 b''
1 """Base class for notebook tests."""
1 """Base class for notebook tests."""
2
2
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import sys
5 import sys
6 import time
6 import time
7 import requests
7 import requests
8 from contextlib import contextmanager
8 from contextlib import contextmanager
9 from subprocess import Popen, STDOUT
9 from subprocess import Popen, STDOUT
10 from unittest import TestCase
10 from unittest import TestCase
11
11
12 import nose
12 import nose
13
13
14 from IPython.utils.tempdir import TemporaryDirectory
14 from IPython.utils.tempdir import TemporaryDirectory
15
15
16 MAX_WAITTIME = 30 # seconds to wait for notebook server to start
16 MAX_WAITTIME = 30 # seconds to wait for notebook server to start
17 POLL_INTERVAL = 0.1 # time between attempts
17 POLL_INTERVAL = 0.1 # time between attempts
18
18
19 # TimeoutError is a builtin on Python 3. This can be removed when we stop
19 # TimeoutError is a builtin on Python 3. This can be removed when we stop
20 # supporting Python 2.
20 # supporting Python 2.
21 class TimeoutError(Exception):
21 class TimeoutError(Exception):
22 pass
22 pass
23
23
24 class NotebookTestBase(TestCase):
24 class NotebookTestBase(TestCase):
25 """A base class for tests that need a running notebook.
25 """A base class for tests that need a running notebook.
26
26
27 This creates an empty profile in a temp ipython_dir
27 This creates an empty profile in a temp ipython_dir
28 and then starts the notebook server with a separate temp notebook_dir.
28 and then starts the notebook server with a separate temp notebook_dir.
29 """
29 """
30
30
31 port = 12341
31 port = 12341
32
32
33 @classmethod
33 @classmethod
34 def wait_until_alive(cls):
34 def wait_until_alive(cls):
35 """Wait for the server to be alive"""
35 """Wait for the server to be alive"""
36 url = 'http://localhost:%i/api/notebooks' % cls.port
36 url = 'http://localhost:%i/api/contents' % cls.port
37 for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)):
37 for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)):
38 try:
38 try:
39 requests.get(url)
39 requests.get(url)
40 except requests.exceptions.ConnectionError:
40 except requests.exceptions.ConnectionError:
41 if cls.notebook.poll() is not None:
41 if cls.notebook.poll() is not None:
42 raise RuntimeError("The notebook server exited with status %s" \
42 raise RuntimeError("The notebook server exited with status %s" \
43 % cls.notebook.poll())
43 % cls.notebook.poll())
44 time.sleep(POLL_INTERVAL)
44 time.sleep(POLL_INTERVAL)
45 else:
45 else:
46 return
46 return
47
47
48 raise TimeoutError("The notebook server didn't start up correctly.")
48 raise TimeoutError("The notebook server didn't start up correctly.")
49
49
50 @classmethod
50 @classmethod
51 def wait_until_dead(cls):
51 def wait_until_dead(cls):
52 """Wait for the server process to terminate after shutdown"""
52 """Wait for the server process to terminate after shutdown"""
53 for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)):
53 for _ in range(int(MAX_WAITTIME/POLL_INTERVAL)):
54 if cls.notebook.poll() is not None:
54 if cls.notebook.poll() is not None:
55 return
55 return
56 time.sleep(POLL_INTERVAL)
56 time.sleep(POLL_INTERVAL)
57
57
58 raise TimeoutError("Undead notebook server")
58 raise TimeoutError("Undead notebook server")
59
59
60 @classmethod
60 @classmethod
61 def setup_class(cls):
61 def setup_class(cls):
62 cls.ipython_dir = TemporaryDirectory()
62 cls.ipython_dir = TemporaryDirectory()
63 cls.notebook_dir = TemporaryDirectory()
63 cls.notebook_dir = TemporaryDirectory()
64 notebook_args = [
64 notebook_args = [
65 sys.executable, '-c',
65 sys.executable, '-c',
66 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
66 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
67 '--port=%d' % cls.port,
67 '--port=%d' % cls.port,
68 '--port-retries=0', # Don't try any other ports
68 '--port-retries=0', # Don't try any other ports
69 '--no-browser',
69 '--no-browser',
70 '--ipython-dir=%s' % cls.ipython_dir.name,
70 '--ipython-dir=%s' % cls.ipython_dir.name,
71 '--notebook-dir=%s' % cls.notebook_dir.name,
71 '--notebook-dir=%s' % cls.notebook_dir.name,
72 ]
72 ]
73 cls.notebook = Popen(notebook_args,
73 cls.notebook = Popen(notebook_args,
74 stdout=nose.iptest_stdstreams_fileno(),
74 stdout=nose.iptest_stdstreams_fileno(),
75 stderr=STDOUT,
75 stderr=STDOUT,
76 )
76 )
77 cls.wait_until_alive()
77 cls.wait_until_alive()
78
78
79 @classmethod
79 @classmethod
80 def teardown_class(cls):
80 def teardown_class(cls):
81 cls.notebook.terminate()
81 cls.notebook.terminate()
82 cls.wait_until_dead()
82 cls.wait_until_dead()
83 cls.ipython_dir.cleanup()
83 cls.ipython_dir.cleanup()
84 cls.notebook_dir.cleanup()
84 cls.notebook_dir.cleanup()
85
85
86 @classmethod
86 @classmethod
87 def base_url(cls):
87 def base_url(cls):
88 return 'http://localhost:%i/' % cls.port
88 return 'http://localhost:%i/' % cls.port
89
89
90
90
91 @contextmanager
91 @contextmanager
92 def assert_http_error(status, msg=None):
92 def assert_http_error(status, msg=None):
93 try:
93 try:
94 yield
94 yield
95 except requests.HTTPError as e:
95 except requests.HTTPError as e:
96 real_status = e.response.status_code
96 real_status = e.response.status_code
97 assert real_status == status, \
97 assert real_status == status, \
98 "Expected status %d, got %d" % (real_status, status)
98 "Expected status %d, got %d" % (real_status, status)
99 if msg:
99 if msg:
100 assert msg in str(e), e
100 assert msg in str(e), e
101 else:
101 else:
102 assert False, "Expected HTTP error status" No newline at end of file
102 assert False, "Expected HTTP error status"
@@ -1,101 +1,101 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 from tornado import web
18 from tornado import web
19 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
19 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
20 from ..utils import url_path_join, url_escape
20 from ..utils import url_path_join, url_escape
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Handlers
23 # Handlers
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26
26
27 class TreeHandler(IPythonHandler):
27 class TreeHandler(IPythonHandler):
28 """Render the tree view, listing notebooks, clusters, etc."""
28 """Render the tree view, listing notebooks, clusters, etc."""
29
29
30 def generate_breadcrumbs(self, path):
30 def generate_breadcrumbs(self, path):
31 breadcrumbs = [(url_escape(url_path_join(self.base_url, 'tree')), '')]
31 breadcrumbs = [(url_escape(url_path_join(self.base_url, 'tree')), '')]
32 comps = path.split('/')
32 comps = path.split('/')
33 ncomps = len(comps)
33 ncomps = len(comps)
34 for i in range(ncomps):
34 for i in range(ncomps):
35 if comps[i]:
35 if comps[i]:
36 link = url_escape(url_path_join(self.base_url, 'tree', *comps[0:i+1]))
36 link = url_escape(url_path_join(self.base_url, 'tree', *comps[0:i+1]))
37 breadcrumbs.append((link, comps[i]))
37 breadcrumbs.append((link, comps[i]))
38 return breadcrumbs
38 return breadcrumbs
39
39
40 def generate_page_title(self, path):
40 def generate_page_title(self, path):
41 comps = path.split('/')
41 comps = path.split('/')
42 if len(comps) > 3:
42 if len(comps) > 3:
43 for i in range(len(comps)-2):
43 for i in range(len(comps)-2):
44 comps.pop(0)
44 comps.pop(0)
45 page_title = url_path_join(*comps)
45 page_title = url_path_join(*comps)
46 if page_title:
46 if page_title:
47 return page_title+'/'
47 return page_title+'/'
48 else:
48 else:
49 return 'Home'
49 return 'Home'
50
50
51 @web.authenticated
51 @web.authenticated
52 def get(self, path='', name=None):
52 def get(self, path='', name=None):
53 path = path.strip('/')
53 path = path.strip('/')
54 nbm = self.notebook_manager
54 cm = self.contents_manager
55 if name is not None:
55 if name is not None:
56 # is a notebook, redirect to notebook handler
56 # is a notebook, redirect to notebook handler
57 url = url_escape(url_path_join(
57 url = url_escape(url_path_join(
58 self.base_url, 'notebooks', path, name
58 self.base_url, 'notebooks', path, name
59 ))
59 ))
60 self.log.debug("Redirecting %s to %s", self.request.path, url)
60 self.log.debug("Redirecting %s to %s", self.request.path, url)
61 self.redirect(url)
61 self.redirect(url)
62 else:
62 else:
63 if not nbm.path_exists(path=path):
63 if not cm.path_exists(path=path):
64 # Directory is hidden or does not exist.
64 # Directory is hidden or does not exist.
65 raise web.HTTPError(404)
65 raise web.HTTPError(404)
66 elif nbm.is_hidden(path):
66 elif cm.is_hidden(path):
67 self.log.info("Refusing to serve hidden directory, via 404 Error")
67 self.log.info("Refusing to serve hidden directory, via 404 Error")
68 raise web.HTTPError(404)
68 raise web.HTTPError(404)
69 breadcrumbs = self.generate_breadcrumbs(path)
69 breadcrumbs = self.generate_breadcrumbs(path)
70 page_title = self.generate_page_title(path)
70 page_title = self.generate_page_title(path)
71 self.write(self.render_template('tree.html',
71 self.write(self.render_template('tree.html',
72 project=self.project_dir,
72 project=self.project_dir,
73 page_title=page_title,
73 page_title=page_title,
74 notebook_path=path,
74 notebook_path=path,
75 breadcrumbs=breadcrumbs
75 breadcrumbs=breadcrumbs
76 ))
76 ))
77
77
78
78
79 class TreeRedirectHandler(IPythonHandler):
79 class TreeRedirectHandler(IPythonHandler):
80 """Redirect a request to the corresponding tree URL"""
80 """Redirect a request to the corresponding tree URL"""
81
81
82 @web.authenticated
82 @web.authenticated
83 def get(self, path=''):
83 def get(self, path=''):
84 url = url_escape(url_path_join(
84 url = url_escape(url_path_join(
85 self.base_url, 'tree', path.strip('/')
85 self.base_url, 'tree', path.strip('/')
86 ))
86 ))
87 self.log.debug("Redirecting %s to %s", self.request.path, url)
87 self.log.debug("Redirecting %s to %s", self.request.path, url)
88 self.redirect(url)
88 self.redirect(url)
89
89
90
90
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92 # URL to handler mappings
92 # URL to handler mappings
93 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
94
94
95
95
96 default_handlers = [
96 default_handlers = [
97 (r"/tree%s" % notebook_path_regex, TreeHandler),
97 (r"/tree%s" % notebook_path_regex, TreeHandler),
98 (r"/tree%s" % path_regex, TreeHandler),
98 (r"/tree%s" % path_regex, TreeHandler),
99 (r"/tree", TreeHandler),
99 (r"/tree", TreeHandler),
100 (r"", TreeRedirectHandler),
100 (r"", TreeRedirectHandler),
101 ]
101 ]
General Comments 0
You need to be logged in to leave comments. Login now